package service

import (
	"bytes"
	"context"
	"crypto/rand"
	"crypto/rsa"
	"crypto/sha256"
	"crypto/x509"
	"crypto/x509/pkix"
	"database/sql"
	_ "embed"
	"encoding/asn1"
	"encoding/base64"
	"encoding/json"
	"encoding/pem"
	"encoding/xml"
	"errors"
	"fmt"
	"io"
	"math/big"
	"mime/multipart"
	"net/http"
	"net/http/httptest"
	"net/url"
	"os"
	"path/filepath"
	"regexp"
	"slices"
	"sort"
	"strconv"
	"strings"
	"sync"
	"sync/atomic"
	"testing"
	"time"

	"github.com/fleetdm/fleet/v4/server/mdm/android"
	android_mock "github.com/fleetdm/fleet/v4/server/mdm/android/mock"
	android_service "github.com/fleetdm/fleet/v4/server/mdm/android/service"
	"github.com/fleetdm/fleet/v4/server/mdm/android/service/androidmgmt"
	"github.com/fleetdm/fleet/v4/server/mdm/android/tests"
	"google.golang.org/api/androidmanagement/v1"

	eeservice "github.com/fleetdm/fleet/v4/ee/server/service"
	"github.com/fleetdm/fleet/v4/pkg/file"
	"github.com/fleetdm/fleet/v4/pkg/fleetdbase"
	shared_mdm "github.com/fleetdm/fleet/v4/pkg/mdm"
	"github.com/fleetdm/fleet/v4/pkg/mdm/mdmtest"
	"github.com/fleetdm/fleet/v4/pkg/optjson"
	"github.com/fleetdm/fleet/v4/server/bindata"
	"github.com/fleetdm/fleet/v4/server/config"
	"github.com/fleetdm/fleet/v4/server/datastore/mysql"
	"github.com/fleetdm/fleet/v4/server/datastore/redis/redistest"
	"github.com/fleetdm/fleet/v4/server/datastore/s3"
	"github.com/fleetdm/fleet/v4/server/fleet"
	"github.com/fleetdm/fleet/v4/server/live_query/live_query_mock"
	servermdm "github.com/fleetdm/fleet/v4/server/mdm"
	apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
	"github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig"
	"github.com/fleetdm/fleet/v4/server/mdm/apple/vpp"
	microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
	"github.com/fleetdm/fleet/v4/server/mdm/microsoft/syncml"
	nanodep_client "github.com/fleetdm/fleet/v4/server/mdm/nanodep/client"
	"github.com/fleetdm/fleet/v4/server/mdm/nanodep/godep"
	nanodep_storage "github.com/fleetdm/fleet/v4/server/mdm/nanodep/storage"
	"github.com/fleetdm/fleet/v4/server/mdm/nanodep/tokenpki"
	"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
	"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/push"
	nanomdm_pushsvc "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/push/service"
	scepserver "github.com/fleetdm/fleet/v4/server/mdm/scep/server"
	mdmtesting "github.com/fleetdm/fleet/v4/server/mdm/testing_utils"
	"github.com/fleetdm/fleet/v4/server/ptr"
	"github.com/fleetdm/fleet/v4/server/service/contract"
	"github.com/fleetdm/fleet/v4/server/service/integrationtest/scep_server"
	"github.com/fleetdm/fleet/v4/server/service/mock"
	"github.com/fleetdm/fleet/v4/server/service/modules/activities"
	"github.com/fleetdm/fleet/v4/server/service/osquery_utils"
	"github.com/fleetdm/fleet/v4/server/service/schedule"
	"github.com/fleetdm/fleet/v4/server/test"
	"github.com/fleetdm/fleet/v4/server/worker"
	kitlog "github.com/go-kit/log"
	"github.com/google/uuid"
	"github.com/jmoiron/sqlx"
	micromdm "github.com/micromdm/micromdm/mdm/mdm"
	"github.com/micromdm/plist"
	"github.com/smallstep/pkcs7"
	"github.com/smallstep/scep"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/stretchr/testify/suite"
	"software.sslmate.com/src/go-pkcs12"
)

func TestIntegrationsMDM(t *testing.T) {
	testingSuite := new(integrationMDMTestSuite)
	testingSuite.withServer.s = &testingSuite.Suite
	suite.Run(t, testingSuite)
}

type integrationMDMTestSuite struct {
	suite.Suite
	withServer
	fleetCfg                   config.FleetConfig
	fleetDMNextCSRStatus       atomic.Value
	pushProvider               *mock.APNSPushProvider
	depStorage                 nanodep_storage.AllDEPStorage
	profileSchedule            *schedule.Schedule
	integrationsSchedule       *schedule.Schedule
	cleanupsSchedule           *schedule.Schedule
	onProfileJobDone           func() // function called when profileSchedule.Trigger() job completed
	onIntegrationsScheduleDone func() // function called when integrationsSchedule.Trigger() job completed
	onCleanupScheduleDone      func() // function called when cleanupsSchedule.Trigger() job completed
	mdmStorage                 *mysql.NanoMDMStorage
	worker                     *worker.Worker
	// Flag to skip jobs processing by worker
	skipWorkerJobs            atomic.Bool
	mdmCommander              *apple_mdm.MDMAppleCommander
	logger                    kitlog.Logger
	scepChallenge             string
	appleVPPConfigSrv         *httptest.Server
	appleVPPConfigSrvConfig   *appleVPPConfigSrvConf
	appleITunesSrv            *httptest.Server
	appleITunesSrvData        map[string]string
	appleGDMFSrv              *httptest.Server
	mockedDownloadFleetdmMeta fleetdbase.Metadata
	scepConfig                *eeservice.SCEPConfigService
	androidAPIClient          *android_mock.Client
	proxyCallbackURL          string
}

// appleVPPConfigSrvConf is used to configure the mock server that mocks Apple's VPP endpoints.
type appleVPPConfigSrvConf struct {
	Assets        []vpp.Asset
	SerialNumbers []string
	Location      string
}

var defaultVPPAssetList = []vpp.Asset{
	{
		AdamID:         "1",
		PricingParam:   "STDQ",
		AvailableCount: 12,
	},
	{
		AdamID:         "2",
		PricingParam:   "STDQ",
		AvailableCount: 3,
	},
	{
		AdamID:         "3",
		PricingParam:   "STDQ",
		AvailableCount: 1,
	},
}

func (s *integrationMDMTestSuite) SetupSuite() {
	s.withDS.SetupSuite("integrationMDMTestSuite")

	appConf, err := s.ds.AppConfig(context.Background())
	require.NoError(s.T(), err)
	appConf.MDM.EnabledAndConfigured = true
	appConf.MDM.WindowsEnabledAndConfigured = true
	appConf.MDM.AppleBMEnabledAndConfigured = true
	appConf.MDM.AndroidEnabledAndConfigured = true
	appConf.MDM.MacOSSetup.BootstrapPackage.Set = false
	err = s.ds.SaveAppConfig(context.Background(), appConf)
	require.NoError(s.T(), err)

	fleetCfg := config.TestConfig()
	testCert, testKey, err := apple_mdm.NewSCEPCACertKey()
	require.NoError(s.T(), err)
	testCertPEM := tokenpki.PEMCertificate(testCert.Raw)
	testKeyPEM := tokenpki.PEMRSAPrivateKey(testKey)
	config.SetTestMDMConfig(s.T(), &fleetCfg, testCertPEM, testKeyPEM, "../../server/service/testdata")
	fleetCfg.Osquery.EnrollCooldown = 0
	fleetCfg.Server.VPPVerifyRequestDelay = 0 * time.Second // Remove the delay to speed up tests
	fleetCfg.Server.VPPVerifyTimeout = fleet.DefaultVPPInstallVerifyTimeout

	mdmStorage, err := s.ds.NewMDMAppleMDMStorage()
	require.NoError(s.T(), err)
	depStorage, err := s.ds.NewMDMAppleDEPStorage()
	require.NoError(s.T(), err)
	scepStorage, err := s.ds.NewSCEPDepot()
	require.NoError(s.T(), err)

	pushLog := kitlog.NewJSONLogger(os.Stdout)
	if os.Getenv("FLEET_INTEGRATION_TESTS_DISABLE_LOG") != "" {
		pushLog = kitlog.NewNopLogger()
	}
	pushFactory, pushProvider := newMockAPNSPushProviderFactory()
	mdmPushService := nanomdm_pushsvc.New(
		mdmStorage,
		mdmStorage,
		pushFactory,
		NewNanoMDMLogger(pushLog),
	)
	mdmCommander := apple_mdm.NewMDMAppleCommander(mdmStorage, mdmPushService)
	s.redisPool = redistest.SetupRedis(s.T(), "zz", false, false, false)
	s.withServer.lq = live_query_mock.New(s.T())

	wlog := kitlog.NewJSONLogger(os.Stdout)
	if os.Getenv("FLEET_INTEGRATION_TESTS_DISABLE_LOG") != "" {
		wlog = kitlog.NewNopLogger()
	}

	activityModule := activities.NewActivityModule(s.ds, wlog)
	androidMockClient := &android_mock.Client{}
	androidMockClient.SetAuthenticationSecretFunc = func(secret string) error {
		return nil
	}
	androidSvc, err := android_service.NewServiceWithClient(wlog, s.ds, androidMockClient, "test-private-key", s.ds, activityModule)
	if err != nil {
		panic(err)
	}
	androidSvc.(*android_service.Service).AllowLocalhostServerURL = true

	macosJob := &worker.MacosSetupAssistant{
		Datastore:  s.ds,
		Log:        wlog,
		DEPService: apple_mdm.NewDEPService(s.ds, depStorage, wlog),
		DEPClient:  apple_mdm.NewDEPClient(depStorage, s.ds, wlog),
	}
	appleMDMJob := &worker.AppleMDM{
		Datastore: s.ds,
		Log:       wlog,
		Commander: mdmCommander,
	}
	vppVerifyJob := &worker.AppleSoftware{
		Datastore: s.ds,
		Log:       wlog,
		Commander: mdmCommander,
	}
	softwareWorker := &worker.SoftwareWorker{
		Datastore:     s.ds,
		Log:           wlog,
		AndroidModule: androidSvc,
	}
	workr := worker.NewWorker(s.ds, wlog)
	workr.TestIgnoreUnknownJobs = true
	workr.Register(macosJob, appleMDMJob, vppVerifyJob, softwareWorker)

	s.worker = workr

	// clear the jobs queue of any pending jobs generated via DB migrations
	mysql.ExecAdhocSQL(s.T(), s.ds, func(q sqlx.ExtContext) error {
		_, err := q.ExecContext(context.Background(), "DELETE FROM jobs")
		return err
	})

	var integrationsSchedule *schedule.Schedule
	var profileSchedule *schedule.Schedule
	var cleanupsSchedule *schedule.Schedule
	cronLog := kitlog.NewJSONLogger(os.Stdout)
	if os.Getenv("FLEET_INTEGRATION_TESTS_DISABLE_LOG") != "" {
		cronLog = kitlog.NewNopLogger()
	}
	serverLogger := kitlog.NewJSONLogger(os.Stdout)
	if os.Getenv("FLEET_INTEGRATION_TESTS_DISABLE_LOG") != "" {
		serverLogger = kitlog.NewNopLogger()
	}

	var softwareInstallerStore fleet.SoftwareInstallerStore
	var bootstrapPackageStore fleet.MDMBootstrapPackageStore
	_, minioEnabled := os.LookupEnv("MINIO_STORAGE_TEST")
	if wantStore := os.Getenv("FLEET_INTEGRATION_TESTS_SOFTWARE_INSTALLER_STORE"); minioEnabled &&
		(wantStore == "s3" || (wantStore == "" && time.Now().UnixNano()%2 == 0)) {

		s.T().Log(">>> using S3/minio software installer store")
		softwareInstallerStore = s3.SetupTestSoftwareInstallerStore(s.T(), "integration-tests", "")
		bootstrapPackageStore = s3.SetupTestBootstrapPackageStore(s.T(), "integration-tests", "")
	}
	scepTimeout := ptr.Duration(10 * time.Second)
	s.scepConfig = eeservice.NewSCEPConfigService(serverLogger, scepTimeout).(*eeservice.SCEPConfigService)

	serverConfig := TestServerOpts{
		License: &fleet.LicenseInfo{
			Tier: fleet.TierPremium,
		},
		Logger:                serverLogger,
		FleetConfig:           &fleetCfg,
		MDMStorage:            mdmStorage,
		DEPStorage:            depStorage,
		SCEPStorage:           scepStorage,
		MDMPusher:             mdmPushService,
		Pool:                  s.redisPool,
		Lq:                    s.lq,
		SoftwareInstallStore:  softwareInstallerStore,
		BootstrapPackageStore: bootstrapPackageStore,
		androidMockClient:     androidMockClient,
		androidModule:         androidSvc,
		StartCronSchedules: []TestNewScheduleFunc{
			func(ctx context.Context, ds fleet.Datastore) fleet.NewCronScheduleFunc {
				return func() (fleet.CronSchedule, error) {
					const name = string(fleet.CronMDMAppleProfileManager)
					logger := cronLog
					profileSchedule = schedule.New(
						ctx, name, s.T().Name(), 1*time.Hour, ds, ds,
						schedule.WithLogger(logger),
						schedule.WithJob("manage_apple_profiles", func(ctx context.Context) error {
							logger.Log("msg", "Starting manage_apple_profiles job", "test", s.T().Name(), "time", time.Now().Format(time.RFC3339))
							if s.onProfileJobDone != nil {
								defer func() {
									logger.Log("msg", "Completing manage_apple_profiles job", "test", s.T().Name(), "time",
										time.Now().Format(time.RFC3339))
									s.onProfileJobDone()
								}()
							}
							err = ReconcileAppleProfiles(ctx, ds, mdmCommander, logger)
							require.NoError(s.T(), err)
							return err
						}),
						schedule.WithJob("manage_apple_declarations", func(ctx context.Context) error {
							logger.Log("msg", "Starting manage_apple_declarations job", "test", s.T().Name(), "time", time.Now().Format(time.RFC3339))
							if s.onProfileJobDone != nil {
								defer func() {
									logger.Log("msg", "Completing manage_apple_declarations job", "test", s.T().Name(), "time",
										time.Now().Format(time.RFC3339))
									s.onProfileJobDone()
								}()
							}
							err = ReconcileAppleDeclarations(ctx, ds, mdmCommander, logger)
							require.NoError(s.T(), err)
							return err
						}),
						schedule.WithJob("manage_windows_profiles", func(ctx context.Context) error {
							logger.Log("msg", "Starting manage_windows_profiles job", "test", s.T().Name(), "time", time.Now().Format(time.RFC3339))
							if s.onProfileJobDone != nil {
								defer func() {
									logger.Log("msg", "Completing manage_windows_profiles job", "test", s.T().Name(), "time",
										time.Now().Format(time.RFC3339))
									s.onProfileJobDone()
								}()
							}
							err := ReconcileWindowsProfiles(ctx, ds, logger)
							require.NoError(s.T(), err)
							return err
						}),
					)
					return profileSchedule, nil
				}
			},
			func(ctx context.Context, ds fleet.Datastore) fleet.NewCronScheduleFunc {
				return func() (fleet.CronSchedule, error) {
					const name = string(fleet.CronWorkerIntegrations)
					logger := cronLog
					integrationsSchedule = schedule.New(
						ctx, name, s.T().Name(), 1*time.Minute, ds, ds,
						schedule.WithLogger(logger),
						schedule.WithJob("integrations_worker", func(ctx context.Context) error {
							if s.skipWorkerJobs.Load() {
								return nil
							}
							return s.worker.ProcessJobs(ctx)
						}),
						schedule.WithJob("dep_cooldowns", func(ctx context.Context) error {
							if s.skipWorkerJobs.Load() {
								return nil
							}
							if s.onIntegrationsScheduleDone != nil {
								defer s.onIntegrationsScheduleDone()
							}

							return worker.ProcessDEPCooldowns(ctx, ds, logger)
						}),
					)
					return integrationsSchedule, nil
				}
			},
			func(ctx context.Context, ds fleet.Datastore) fleet.NewCronScheduleFunc {
				return func() (fleet.CronSchedule, error) {
					const name = string(fleet.CronCleanupsThenAggregation)
					logger := cronLog
					cleanupsSchedule = schedule.New(
						ctx, name, s.T().Name(), 1*time.Hour, ds, ds,
						schedule.WithLogger(logger),
						schedule.WithJob("cleanup_android_enterprise", func(ctx context.Context) error {
							if s.onCleanupScheduleDone != nil {
								defer s.onCleanupScheduleDone()
							}
							return androidSvc.VerifyExistingEnterpriseIfAny(ctx)
						}),
					)
					return cleanupsSchedule, nil
				}
			},
			func(ctx context.Context, ds fleet.Datastore) fleet.NewCronScheduleFunc {
				return func() (fleet.CronSchedule, error) {
					const name = string(fleet.CronAppleMDMIPhoneIPadRefetcher)
					logger := cronLog
					refetcherSchedule := schedule.New(
						ctx, name, s.T().Name(), 1*time.Hour, ds, ds,
						schedule.WithLogger(logger),
						schedule.WithJob("cron_iphone_ipad_refetcher", func(ctx context.Context) error {
							return apple_mdm.IOSiPadOSRefetch(ctx, ds, mdmCommander, logger, func(ctx context.Context, user *fleet.User, act fleet.ActivityDetails) error {
								return newActivity(ctx, user, act, ds, logger)
							})
						}),
					)
					return refetcherSchedule, nil
				}
			},
		},
		APNSTopic:         "com.apple.mgmt.External.10ac3ce5-4668-4e58-b69a-b2b5ce667589",
		EnableSCEPProxy:   true,
		WithDEPWebview:    true,
		SCEPConfigService: s.scepConfig,
		EnableSCIM:        true,
	}

	// ensure all our tests support challenges with invalid XML characters
	s.scepChallenge = "scepcha/><llenge"
	err = s.ds.InsertMDMConfigAssets(context.Background(), []fleet.MDMConfigAsset{
		{Name: fleet.MDMAssetSCEPChallenge, Value: []byte(s.scepChallenge)},
	}, nil)
	require.NoError(s.T(), err)

	svc, ctx := NewTestService(s.T(), s.ds, fleetCfg, &serverConfig)
	// This is a bit of a code smell but I don't see a better way to initialize this for the tests. The
	// initialization pattern works fine in our normal fleet server setup
	appleMDMJob.VPPInstaller = svc

	users, server := RunServerForTestsWithServiceWithDS(s.T(), ctx, s.ds, svc, &serverConfig)

	s.server = server
	s.users = users
	s.token = s.getTestAdminToken()
	s.cachedAdminToken = s.token
	s.fleetCfg = fleetCfg
	s.pushProvider = pushProvider
	s.depStorage = depStorage
	s.integrationsSchedule = integrationsSchedule
	s.profileSchedule = profileSchedule
	s.cleanupsSchedule = cleanupsSchedule
	s.mdmStorage = mdmStorage
	s.mdmCommander = mdmCommander
	s.logger = serverLogger
	s.androidAPIClient = androidMockClient

	fleetdmSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		status := s.fleetDMNextCSRStatus.Swap(http.StatusOK)
		w.WriteHeader(status.(int))
		resp := []byte(fmt.Sprintf("status: %d", status))
		if status == http.StatusOK && strings.Contains(r.URL.RawQuery, "deliveryMethod=json") {
			rawBody, err := io.ReadAll(r.Body)
			require.NoError(s.T(), err)
			var req struct {
				UnsignedCSRData []byte `json:"unsignedCsrData"`
			}
			err = json.Unmarshal(rawBody, &req)
			require.NoError(s.T(), err)

			resp = []byte(
				fmt.Sprintf(
					`{"csr": %q}`,
					base64.StdEncoding.EncodeToString(req.UnsignedCSRData),
				),
			)
		}
		_, _ = w.Write(resp)
	}))

	if s.appleVPPConfigSrvConfig == nil {
		s.appleVPPConfigSrvConfig = &appleVPPConfigSrvConf{
			Assets:        defaultVPPAssetList,
			SerialNumbers: []string{"123", "456"},
			Location:      "Fleet Location One",
		}
	}

	s.appleVPPConfigSrv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Handle /associate
		if strings.Contains(r.URL.Path, "associate") {
			var associations vpp.AssociateAssetsRequest

			decoder := json.NewDecoder(r.Body)
			if err := decoder.Decode(&associations); err != nil {
				http.Error(w, "invalid request", http.StatusBadRequest)
				return
			}

			fmt.Printf("Mock VPP Server: Trying to associate %v with %v\n", associations.SerialNumbers, associations.Assets)

			if len(associations.Assets) == 0 {
				w.Header().Set("Content-Type", "application/json")
				w.WriteHeader(http.StatusBadRequest)
				res := vpp.ErrorResponse{
					ErrorNumber:  9718,
					ErrorMessage: "This request doesn't contain an asset, which is a required argument. Change the request to provide an asset.",
				}
				if err := json.NewEncoder(w).Encode(res); err != nil {
					panic(err)
				}
				return
			}

			if len(associations.SerialNumbers) == 0 {
				w.Header().Set("Content-Type", "application/json")
				w.WriteHeader(http.StatusBadRequest)
				res := vpp.ErrorResponse{
					ErrorNumber:  9719,
					ErrorMessage: "Either clientUserIds or serialNumbers are required arguments. Change the request to provide assignable users and devices.",
				}
				if err := json.NewEncoder(w).Encode(res); err != nil {
					panic(err)
				}
				return
			}

			var badAssets []vpp.Asset
			for _, reqAsset := range associations.Assets {
				var found bool
				for _, goodAsset := range s.appleVPPConfigSrvConfig.Assets {
					if reqAsset == goodAsset {
						found = true
					}
				}
				if !found {
					badAssets = append(badAssets, reqAsset)
				}
			}

			var badSerials []string
			for _, reqSerial := range associations.SerialNumbers {
				var found bool
				for _, goodSerial := range s.appleVPPConfigSrvConfig.SerialNumbers {
					if reqSerial == goodSerial {
						found = true
					}
				}
				if !found {
					badSerials = append(badSerials, reqSerial)
				}
			}

			if len(badAssets) != 0 || len(badSerials) != 0 {
				errMsg := "error associating assets."
				if len(badAssets) > 0 {
					var badAdamIds []string
					for _, asset := range badAssets {
						badAdamIds = append(badAdamIds, asset.AdamID)
					}
					errMsg += fmt.Sprintf(" assets don't exist on account: %s.", strings.Join(badAdamIds, ", "))
				}
				if len(badSerials) > 0 {
					errMsg += fmt.Sprintf(" bad serials: %s.", strings.Join(badSerials, ", "))
				}
				res := vpp.ErrorResponse{
					ErrorInfo: vpp.ResponseErrorInfo{
						Assets:        badAssets,
						ClientUserIds: []string{"something"},
						SerialNumbers: badSerials,
					},
					// Not sure what error should be returned on each
					// error type
					ErrorNumber:  1,
					ErrorMessage: errMsg,
				}
				w.Header().Set("Content-Type", "application/json")
				w.WriteHeader(http.StatusBadRequest)
				if err := json.NewEncoder(w).Encode(res); err != nil {
					panic(err)
				}
			}
			_, _ = w.Write([]byte(`{"eventId": "123-345"}`))
			return
		}

		// Handle /assets
		if strings.Contains(r.URL.Path, "assets") {
			w.Header().Set("Content-Type", "application/json")
			adamID := r.URL.Query().Get("adamId")
			var assets []vpp.Asset
			switch adamID {
			case "":
				assets = s.appleVPPConfigSrvConfig.Assets
			default:
				for _, a := range s.appleVPPConfigSrvConfig.Assets {
					if a.AdamID == adamID {
						assets = []vpp.Asset{a}
					}
				}
			}
			encoder := json.NewEncoder(w)
			err := encoder.Encode(map[string][]vpp.Asset{"assets": assets})
			if err != nil {
				panic(err)
			}
			return
		}

		// Handle /client/config
		resp := []byte(fmt.Sprintf(`{"locationName": "%s"}`, s.appleVPPConfigSrvConfig.Location))
		if strings.Contains(r.URL.RawQuery, "invalidToken") {
			// This replicates the response sent back from Apple's VPP endpoints when an invalid
			// token is passed. For more details see:
			// https://developer.apple.com/documentation/devicemanagement/app_and_book_management/app_and_book_management_legacy/interpreting_error_codes
			// https://developer.apple.com/documentation/devicemanagement/client_config
			// https://developer.apple.com/documentation/devicemanagement/errorresponse
			// Note that the Apple server returns 200 in this case.
			resp = []byte(`{"errorNumber": 9622,"errorMessage": "Invalid authentication token"}`)
		}

		if strings.Contains(r.URL.RawQuery, "serverError") {
			resp = []byte(`{"errorNumber": 9603,"errorMessage": "Internal server error"}`)
			w.WriteHeader(http.StatusInternalServerError)
		}

		_, _ = w.Write(resp)
	}))

	s.appleITunesSrvData = map[string]string{
		// macOS app
		"1": `{"bundleId": "a-1", "artworkUrl512": "https://example.com/images/1", "version": "1.0.0", "trackName": "App 1", "TrackID": 1}`,
		// macOS, iOS, iPadOS app
		"2": `{"bundleId": "b-2", "artworkUrl512": "https://example.com/images/2", "version": "2.0.0", "trackName": "App 2", "TrackID": 2, "supportedDevices": ["MacDesktop-MacDesktop", "iPhone5s-iPhone5s", "iPadAir-iPadAir"] }`,
		// iPadOS app
		"3": `{"bundleId": "c-3", "artworkUrl512": "https://example.com/images/3", "version": "3.0.0", "trackName": "App 3", "TrackID": 3, "supportedDevices": ["iPadAir-iPadAir"] }`,

		"4": `{"bundleId": "d-4", "artworkUrl512": "https://example.com/images/4", "version": "4.0.0", "trackName": "App 4", "TrackID": 4}`,
		// App with 0 licenses
		"5": `{"bundleId": "e-5", "artworkUrl512": "https://example.com/images/5", "version": "5.0.0", "trackName": "App 5", "TrackID": 5}`,
	}

	s.appleITunesSrv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// a map of apps we can respond with

		adamIDString := r.URL.Query().Get("id")
		adamIDs := strings.Split(adamIDString, ",")

		var objs []string
		for _, a := range adamIDs {
			data, ok := s.appleITunesSrvData[a]
			if !ok {
				continue
			}
			objs = append(objs, data)
		}

		_, _ = w.Write([]byte(fmt.Sprintf(`{"results": [%s]}`, strings.Join(objs, ","))))
	}))

	s.appleGDMFSrv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
		// load the test data from the file
		b, err := os.ReadFile("../mdm/apple/gdmf/testdata/gdmf.json")
		require.NoError(s.T(), err)
		_, err = w.Write(b)
		require.NoError(s.T(), err)
	}))

	s.T().Setenv("FLEET_DEV_GDMF_URL", s.appleGDMFSrv.URL)
	s.T().Setenv("TEST_FLEETDM_API_URL", fleetdmSrv.URL)
	s.T().Setenv("FLEET_DEV_ITUNES_URL", s.appleITunesSrv.URL)

	s.mockedDownloadFleetdmMeta = fleetdbase.Metadata{
		MSIURL:           fmt.Sprintf("https://download-testing.fleetdm.com/archive/stable/%s/fleetd-base.msi", uuid.NewString()),
		MSISha256:        uuid.NewString(),
		PKGURL:           fmt.Sprintf("https://download-testing.fleetdm.com/archive/stable/%s/fleetd-base.pkg", uuid.NewString()),
		PKGSha256:        uuid.NewString(),
		ManifestPlistURL: fmt.Sprintf("https://download-testing.fleetdm.com/archive/stable/%s/fleetd-base-manifest.plist", uuid.NewString()),
		Version:          "2024-06-25_03-01-17",
	}
	downloadFleetdmSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == "/stable/meta.json" {
			w.Header().Set("Content-Type", "application/json")
			w.WriteHeader(http.StatusOK)
			require.NoError(s.T(), json.NewEncoder(w).Encode(s.mockedDownloadFleetdmMeta))
		}
	}))
	s.T().Setenv("FLEET_DEV_DOWNLOAD_FLEETDM_URL", downloadFleetdmSrv.URL)
	s.T().Cleanup(downloadFleetdmSrv.Close)

	appConf, err = s.ds.AppConfig(context.Background())
	require.NoError(s.T(), err)
	appConf.ServerSettings.ServerURL = server.URL
	appConf.Features.EnableSoftwareInventory = true
	err = s.ds.SaveAppConfig(context.Background(), appConf)
	require.NoError(s.T(), err)

	// enable MDM flows
	s.appleCoreCertsSetup()

	// create a global enroll secret
	var applyResp applyEnrollSecretSpecResponse
	s.DoJSON("POST", "/api/latest/fleet/spec/enroll_secret", applyEnrollSecretSpecRequest{
		Spec: &fleet.EnrollSecretSpec{
			Secrets: []*fleet.EnrollSecret{{Secret: "global-secret"}},
		},
	}, http.StatusOK, &applyResp)

	s.T().Cleanup(fleetdmSrv.Close)
	s.T().Cleanup(s.appleVPPConfigSrv.Close)
	s.T().Cleanup(s.appleITunesSrv.Close)
	s.T().Cleanup(s.appleGDMFSrv.Close)
}

func (s *integrationMDMTestSuite) TearDownSuite() {
	appConf, err := s.ds.AppConfig(context.Background())
	require.NoError(s.T(), err)
	appConf.MDM.EnabledAndConfigured = false
	err = s.ds.SaveAppConfig(context.Background(), appConf)
	require.NoError(s.T(), err)
}

func (s *integrationMDMTestSuite) FailNextCSRRequestWith(status int) {
	s.fleetDMNextCSRStatus.Store(status)
}

func (s *integrationMDMTestSuite) SucceedNextCSRRequest() {
	s.fleetDMNextCSRStatus.Store(http.StatusOK)
}

func (s *integrationMDMTestSuite) TearDownTest() {
	t := s.T()
	ctx := context.Background()

	s.token = s.getTestAdminToken()
	appCfg := s.getConfig()
	// ensure windows mdm is always enabled for the next test
	appCfg.MDM.WindowsEnabledAndConfigured = true
	// ensure global disk encryption is disabled on exit
	appCfg.MDM.EnableDiskEncryption = optjson.SetBool(false)
	// ensure enable release manually is false
	appCfg.MDM.MacOSSetup.EnableReleaseDeviceManually = optjson.SetBool(false)
	// ensure global Windows OS updates are always disabled for the next test
	appCfg.MDM.WindowsUpdates = fleet.WindowsUpdates{}
	// ensure the server URL is constant
	appCfg.ServerSettings.ServerURL = s.server.URL
	err := s.ds.SaveAppConfig(ctx, &appCfg.AppConfig)
	require.NoError(t, err)

	s.withServer.commonTearDownTest(t)

	// use a sql statement to delete all profiles, since the datastore prevents
	// deleting the fleet-specific ones.
	mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
		_, err := q.ExecContext(ctx, "DELETE FROM mdm_apple_configuration_profiles")
		return err
	})
	mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
		_, err := q.ExecContext(ctx, "DELETE FROM mdm_windows_configuration_profiles")
		return err
	})
	mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
		_, err := q.ExecContext(ctx, "DELETE FROM mdm_apple_bootstrap_packages")
		return err
	})
	mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
		_, err := q.ExecContext(ctx, "DELETE FROM mdm_android_configuration_profiles")
		return err
	})

	// Delete any CAs
	mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
		_, err := q.ExecContext(ctx, "DELETE FROM certificate_authorities")
		return err
	})

	// clear any pending worker job
	mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
		_, err := q.ExecContext(ctx, "DELETE FROM jobs")
		return err
	})

	// clear any host dep assignments
	mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
		_, err := q.ExecContext(ctx, "DELETE FROM host_dep_assignments")
		return err
	})

	// clear any mdm windows enrollments
	mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
		_, err := q.ExecContext(ctx, "DELETE FROM mdm_windows_enrollments")
		return err
	})

	// clear any lingering declarations
	mysql.ExecAdhocSQL(t, s.ds, func(tx sqlx.ExtContext) error {
		_, err := tx.ExecContext(ctx, "DELETE FROM mdm_apple_declarations")
		return err
	})
	mysql.ExecAdhocSQL(t, s.ds, func(tx sqlx.ExtContext) error {
		_, err := tx.ExecContext(ctx, "DELETE FROM host_mdm_apple_declarations")
		return err
	})
	mysql.ExecAdhocSQL(t, s.ds, func(tx sqlx.ExtContext) error {
		_, err := tx.ExecContext(ctx, "DELETE FROM mobile_device_management_solutions;")
		return err
	})
	mysql.ExecAdhocSQL(t, s.ds, func(tx sqlx.ExtContext) error {
		_, err := tx.ExecContext(ctx, "DELETE FROM host_mdm;")
		return err
	})
	mysql.ExecAdhocSQL(t, s.ds, func(tx sqlx.ExtContext) error {
		_, err := tx.ExecContext(ctx, "DELETE FROM abm_tokens;")
		return err
	})
	mysql.ExecAdhocSQL(t, s.ds, func(tx sqlx.ExtContext) error {
		_, err := tx.ExecContext(ctx, "DELETE FROM vpp_tokens;")
		return err
	})
	mysql.ExecAdhocSQL(t, s.ds, func(tx sqlx.ExtContext) error {
		_, err := tx.ExecContext(ctx, "DELETE FROM setup_experience_status_results;")
		return err
	})
	mysql.ExecAdhocSQL(t, s.ds, func(tx sqlx.ExtContext) error {
		_, err := tx.ExecContext(ctx, "DELETE FROM setup_experience_scripts;")
		return err
	})

	mysql.ExecAdhocSQL(t, s.ds, func(tx sqlx.ExtContext) error {
		_, err := tx.ExecContext(ctx, "DELETE FROM vpp_apps;")
		return err
	})

	err = s.ds.DeleteAllEnterprises(ctx)
	require.NoError(t, err)

	s.androidAPIClient.EnterprisesPoliciesModifyPolicyApplicationsFuncInvoked = false
	s.androidAPIClient.EnterprisesPoliciesPatchFuncInvoked = false
	s.androidAPIClient.EnterprisesDevicesPatchFuncInvoked = false
}

func (s *integrationMDMTestSuite) mockDEPResponse(orgName string, handler http.Handler) {
	t := s.T()
	srv := httptest.NewServer(handler)
	err := s.depStorage.StoreConfig(context.Background(), orgName, &nanodep_client.Config{BaseURL: srv.URL})
	depSvc := apple_mdm.NewDEPService(s.ds, s.depStorage, s.logger)
	require.NoError(t, depSvc.CreateDefaultAutomaticProfile(context.Background()))
	require.NoError(t, err)
	t.Cleanup(func() {
		srv.Close()
		err := s.depStorage.StoreConfig(context.Background(), orgName, &nanodep_client.Config{BaseURL: nanodep_client.DefaultBaseURL})
		require.NoError(t, err)
	})
}

func (s *integrationMDMTestSuite) awaitTriggerProfileSchedule(t *testing.T) {
	// three jobs running sequentially (macOS profiles and declarations, then Windows) on the same schedule
	t.Logf("[awaitTriggerProfileSchedule] Starting profile schedule trigger at %s", time.Now().Format(time.RFC3339))
	var wg sync.WaitGroup
	wg.Add(3)
	s.onProfileJobDone = wg.Done
	t.Logf("[awaitTriggerProfileSchedule] Triggering profile schedule...")
	_, err := s.profileSchedule.Trigger()
	require.NoError(t, err)
	t.Logf("[awaitTriggerProfileSchedule] Waiting for 3 jobs to complete...")
	// Add timeout detection
	done := make(chan struct{})
	go func() {
		wg.Wait()
		close(done)
	}()

	select {
	case <-done:
		t.Logf("[awaitTriggerProfileSchedule] All jobs completed successfully at %s", time.Now().Format(time.RFC3339))
	case <-time.After(5 * time.Minute):
		t.Fatalf("Profile schedule jobs timed out after 5 minutes")
	}
}

func (s *integrationMDMTestSuite) TestUserAuthenticate() {
	t := s.T()
	mdmDevice := mdmtest.NewTestMDMClientAppleDirect(mdmtest.AppleEnrollInfo{
		SCEPChallenge: s.scepChallenge,
		SCEPURL:       s.server.URL + apple_mdm.SCEPPath,
		MDMURL:        s.server.URL + apple_mdm.MDMPath,
	}, "MacBookPro16,1")
	err := mdmDevice.Enroll()
	require.NoError(t, err)

	err = mdmDevice.UserAuthenticate()
	require.ErrorContains(t, err, "410 Gone")
}

func (s *integrationMDMTestSuite) TestGetBootstrapToken() {
	// see https://developer.apple.com/documentation/devicemanagement/get_bootstrap_token
	t := s.T()
	mdmDevice := mdmtest.NewTestMDMClientAppleDirect(mdmtest.AppleEnrollInfo{
		SCEPChallenge: s.scepChallenge,
		SCEPURL:       s.server.URL + apple_mdm.SCEPPath,
		MDMURL:        s.server.URL + apple_mdm.MDMPath,
	}, "MacBookPro16,1")
	err := mdmDevice.Enroll()
	require.NoError(t, err)

	checkStoredCertAuthAssociation := func(id string, expectedCount uint) {
		// confirm expected cert auth association
		mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
			var ct uint
			// query duplicates the logic in nanomdm/storage/mysql/certauth.go
			if err := sqlx.GetContext(context.Background(), q, &ct, "SELECT COUNT(*) FROM nano_cert_auth_associations WHERE id = ?", mdmDevice.UUID); err != nil {
				return err
			}
			require.Equal(t, expectedCount, ct)
			return nil
		})
	}
	checkStoredCertAuthAssociation(mdmDevice.UUID, 1)

	checkStoredBootstrapToken := func(id string, expectedToken *string, expectedErr error) {
		// confirm expected bootstrap token
		mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
			var tok *string
			err := sqlx.GetContext(context.Background(), q, &tok, "SELECT bootstrap_token_b64 FROM nano_devices WHERE id = ?", mdmDevice.UUID)
			if err != nil || expectedErr != nil {
				require.ErrorIs(t, err, expectedErr)
			} else {
				require.NoError(t, err)
			}

			if expectedToken != nil {
				require.NotEmpty(t, tok)
				decoded, err := base64.StdEncoding.DecodeString(*tok)
				require.NoError(t, err)
				require.Equal(t, *expectedToken, string(decoded))
			} else {
				require.Empty(t, tok)
			}
			return nil
		})
	}

	t.Run("bootstrap token not set", func(t *testing.T) {
		// device record exists, but bootstrap token not set
		checkStoredBootstrapToken(mdmDevice.UUID, nil, nil)

		// if token not set, server returns empty body and no error (see https://github.com/micromdm/nanomdm/pull/63)
		res, err := mdmDevice.GetBootstrapToken()
		require.NoError(t, err)
		require.Nil(t, res)
	})

	t.Run("bootstrap token set", func(t *testing.T) {
		// device record exists, set bootstrap token
		token := base64.StdEncoding.EncodeToString([]byte("testtoken"))
		mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
			_, err := q.ExecContext(context.Background(), "UPDATE nano_devices SET bootstrap_token_b64 = ? WHERE id = ?", base64.StdEncoding.EncodeToString([]byte(token)), mdmDevice.UUID)
			require.NoError(t, err)
			return nil
		})
		checkStoredBootstrapToken(mdmDevice.UUID, &token, nil)

		// if token set, server returns token
		res, err := mdmDevice.GetBootstrapToken()
		require.NoError(t, err)
		require.NotNil(t, res)
		require.Equal(t, token, string(res))
	})

	t.Run("no device record", func(t *testing.T) {
		// delete the entire device record
		mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
			_, err := q.ExecContext(context.Background(), "DELETE FROM nano_devices WHERE id = ?", mdmDevice.UUID)
			require.NoError(t, err)
			return nil
		})
		checkStoredBootstrapToken(mdmDevice.UUID, nil, sql.ErrNoRows)

		// if not found, server returns empty body and no error (see https://github.com/fleetdm/nanomdm/pull/8)
		res, err := mdmDevice.GetBootstrapToken()
		require.NoError(t, err)
		require.Nil(t, res)
	})

	t.Run("no cert auth association", func(t *testing.T) {
		// on mdm checkout, nano soft deletes by calling storage.Disable, which leaves the cert auth
		// association in place, so what if we hard delete instead?
		mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
			_, err := q.ExecContext(context.Background(), "DELETE FROM nano_cert_auth_associations WHERE id = ?", mdmDevice.UUID)
			require.NoError(t, err)
			return nil
		})
		checkStoredCertAuthAssociation(mdmDevice.UUID, 0)

		// server returns 403 Forbidden for cert auth failures (enrollment not associated with cert)
		res, err := mdmDevice.GetBootstrapToken()
		require.ErrorContains(t, err, "403") // getbootstraptoken service: cert auth: existing enrollment: enrollment not associated with cert
		require.Nil(t, res)
	})
}

func (s *integrationMDMTestSuite) TestAppleGetAppleMDM() {
	t := s.T()

	var mdmResp getAppleMDMResponse
	s.DoJSON("GET", "/api/latest/fleet/apns", nil, http.StatusOK, &mdmResp)
	// returned values are dummy, this is a test certificate
	require.Equal(t, "Fleet", mdmResp.Issuer)
	require.NotZero(t, mdmResp.SerialNumber)
	require.Equal(t, "Fleet", mdmResp.CommonName)
	require.NotZero(t, mdmResp.RenewDate)

	var countTokensResp countABMTokensResponse
	s.DoJSON("GET", "/api/latest/fleet/abm_tokens/count", nil, http.StatusOK, &countTokensResp)
	assert.EqualValues(t, 0, countTokensResp.Count)

	// set up multiple ABM tokens with different org names
	defaultOrgName := "fleet_test"
	s.enableABM(defaultOrgName)
	tmOrgName := t.Name()
	s.enableABM(tmOrgName)

	var tokensResp listABMTokensResponse
	s.DoJSON("GET", "/api/latest/fleet/abm_tokens", nil, http.StatusOK, &tokensResp)

	// for t.Name()
	tok := s.getABMTokenByName(defaultOrgName, tokensResp.Tokens)
	require.NotNil(t, tok)
	require.False(t, tok.TermsExpired)
	require.Equal(t, "abc", tok.AppleID)
	require.Equal(t, defaultOrgName, tok.OrganizationName)
	require.Equal(t, s.server.URL+"/mdm/apple/mdm", tok.MDMServerURL)
	require.Equal(t, fleet.TeamNameNoTeam, tok.MacOSTeam.Name)
	require.Equal(t, fleet.TeamNameNoTeam, tok.IOSTeam.Name)
	require.Equal(t, fleet.TeamNameNoTeam, tok.IPadOSTeam.Name)

	// for tmOrgName
	tok = s.getABMTokenByName(tmOrgName, tokensResp.Tokens)
	require.NotNil(t, tok)
	require.False(t, tok.TermsExpired)
	require.Equal(t, "abc", tok.AppleID)
	require.Equal(t, tmOrgName, tok.OrganizationName)
	require.Equal(t, s.server.URL+"/mdm/apple/mdm", tok.MDMServerURL)
	require.Equal(t, fleet.TeamNameNoTeam, tok.MacOSTeam.Name)
	require.Equal(t, fleet.TeamNameNoTeam, tok.IOSTeam.Name)
	require.Equal(t, fleet.TeamNameNoTeam, tok.IPadOSTeam.Name)

	s.DoJSON("GET", "/api/latest/fleet/abm_tokens/count", nil, http.StatusOK, &countTokensResp)
	assert.EqualValues(t, 2, countTokensResp.Count)

	// create a new team
	tm, err := s.ds.NewTeam(context.Background(), &fleet.Team{
		Name:        t.Name(),
		Description: "desc",
	})
	require.NoError(t, err)
	// set the default bm assignment for that token to that team
	acResp := appConfigResponse{}
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(fmt.Sprintf(`{
		"mdm": {
			"apple_business_manager": [{
			  "organization_name": %q,
			  "macos_team": %q,
			  "ios_team": %q,
			  "ipados_team": %q
			}]
		}
	}`, tmOrgName, tm.Name, tm.Name, tm.Name)), http.StatusOK, &acResp)
	t.Cleanup(func() {
		s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
			"mdm": {
				"apple_business_manager": []
			}
		}`), http.StatusOK, &acResp)
	})

	// try again, this time we get team assignments in the response
	tokensResp = listABMTokensResponse{}
	s.DoJSON("GET", "/api/latest/fleet/abm_tokens", nil, http.StatusOK, &tokensResp)

	tok = s.getABMTokenByName(tmOrgName, tokensResp.Tokens)
	require.NotNil(t, tok)
	require.False(t, tok.TermsExpired)
	require.Equal(t, "abc", tok.AppleID)
	require.Equal(t, tmOrgName, tok.OrganizationName)
	require.Equal(t, s.server.URL+"/mdm/apple/mdm", tok.MDMServerURL)
	require.Equal(t, tm.Name, tok.MacOSTeam.Name)
	require.Equal(t, tm.Name, tok.IOSTeam.Name)
	require.Equal(t, tm.Name, tok.IPadOSTeam.Name)

	// Reset the teams via app config
	acResp = appConfigResponse{}
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
			"mdm": {
				"apple_business_manager": []
			}
		}`), http.StatusOK, &acResp)

	tokensResp = listABMTokensResponse{}
	s.DoJSON("GET", "/api/latest/fleet/abm_tokens", nil, http.StatusOK, &tokensResp)
	tok = s.getABMTokenByName(tmOrgName, tokensResp.Tokens)
	require.NotNil(t, tok)
	require.False(t, tok.TermsExpired)
	require.Equal(t, "abc", tok.AppleID)
	require.Equal(t, tmOrgName, tok.OrganizationName)
	require.Equal(t, s.server.URL+"/mdm/apple/mdm", tok.MDMServerURL)
	require.Equal(t, fleet.TeamNameNoTeam, tok.MacOSTeam.Name)
	require.Equal(t, fleet.TeamNameNoTeam, tok.IOSTeam.Name)
	require.Equal(t, fleet.TeamNameNoTeam, tok.IPadOSTeam.Name)
}

func (s *integrationMDMTestSuite) getABMTokenByName(orgName string, tokens []*fleet.ABMToken) *fleet.ABMToken {
	for _, tok := range tokens {
		if tok.OrganizationName == orgName {
			return tok
		}
	}

	return nil
}

func (s *integrationMDMTestSuite) TestABMTeamPersistsOnConfigChange() {
	t := s.T()

	s.enableABM(t.Name())

	// Same tests, but for teams
	team, err := s.ds.NewTeam(context.Background(), &fleet.Team{
		Name:        "team1",
		Description: "desc team1",
	})
	require.NoError(t, err)

	tokensResp := listABMTokensResponse{}
	s.DoJSON("GET", "/api/latest/fleet/abm_tokens", nil, http.StatusOK, &tokensResp)
	tok := s.getABMTokenByName(t.Name(), tokensResp.Tokens)

	s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/abm_tokens/%d/teams", tok.ID), json.RawMessage(fmt.Sprintf(`{
    "ios_team_id": %d
	}`, team.ID)), http.StatusOK, &updateABMTokenTeamsResponse{})

	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
    "org_info": {"org_name": "New name"}
	}`), http.StatusOK, &appConfigResponse{})

	s.DoJSON("GET", "/api/latest/fleet/abm_tokens", nil, http.StatusOK, &tokensResp)

	require.Len(t, tokensResp.Tokens, 1)
	assert.Equal(t, team.Name, tokensResp.Tokens[0].IOSTeam.Name)
	assert.Equal(t, team.ID, tokensResp.Tokens[0].IOSTeam.ID)
}

func (s *integrationMDMTestSuite) TestABMExpiredToken() {
	t := s.T()

	s.enableABM(t.Name())

	var returnType string
	s.mockDEPResponse(t.Name(), http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch returnType {
		case "not_signed":
			w.WriteHeader(http.StatusForbidden)
			_, _ = w.Write([]byte(`{"code": "T_C_NOT_SIGNED"}`))
		case "unauthorized":
			w.WriteHeader(http.StatusUnauthorized)
			_, _ = w.Write([]byte(`{}`))
		case "success":
			w.WriteHeader(http.StatusOK)
			_, _ = w.Write([]byte(`{"auth_session_token": "abcd"}`))
		default:
			require.Fail(t, "unexpected return type: %s", returnType)
		}
	}))

	config := s.getConfig()
	require.False(t, config.MDM.AppleBMTermsExpired)

	ctx := context.Background()
	fleetSyncer := apple_mdm.NewDEPService(s.ds, s.depStorage, s.logger)

	// not signed error flips the AppleBMTermsExpired flag
	returnType = "not_signed"
	err := fleetSyncer.RunAssigner(ctx)
	require.ErrorContains(t, err, "T_C_NOT_SIGNED")
	var tokensResp listABMTokensResponse
	s.DoJSON("GET", "/api/latest/fleet/abm_tokens", nil, http.StatusOK, &tokensResp)
	tok := s.getABMTokenByName(t.Name(), tokensResp.Tokens)
	require.NotNil(t, tok)
	require.True(t, tok.TermsExpired)
	config = s.getConfig()
	require.True(t, config.MDM.AppleBMTermsExpired)

	// a successful call clears it
	returnType = "success"
	err = fleetSyncer.RunAssigner(ctx)
	require.NoError(t, err)
	tokensResp = listABMTokensResponse{}
	s.DoJSON("GET", "/api/latest/fleet/abm_tokens", nil, http.StatusOK, &tokensResp)
	tok = s.getABMTokenByName(t.Name(), tokensResp.Tokens)
	require.NotNil(t, tok)
	require.False(t, tok.TermsExpired)

	config = s.getConfig()
	require.False(t, config.MDM.AppleBMTermsExpired)

	// an unauthorized call does not flip the terms expired flag
	returnType = "unauthorized"
	err = fleetSyncer.RunAssigner(ctx)
	require.ErrorContains(t, err, "DEP auth error")
	tokensResp = listABMTokensResponse{}
	s.DoJSON("GET", "/api/latest/fleet/abm_tokens", nil, http.StatusOK, &tokensResp)
	tok = s.getABMTokenByName(t.Name(), tokensResp.Tokens)
	require.NotNil(t, tok)
	require.False(t, tok.TermsExpired)

	config = s.getConfig()
	require.False(t, config.MDM.AppleBMTermsExpired)
}

func checkNextPayloads(t *testing.T, mdmDevice *mdmtest.TestAppleMDMClient, forceDeviceErr bool) ([][]byte, []string) {
	installs := [][]byte{}
	removes := []string{}

	// on the first run, cmd will be nil and we need to
	// ping the server via idle
	// if after idle or acknowledge cmd is still nil, it
	// means there aren't any commands left to run
	cmd, err := mdmDevice.Idle()
	require.NoError(t, err)
	for cmd != nil {
		var fullCmd micromdm.CommandPayload
		switch cmd.Command.RequestType {
		case "InstallProfile":
			require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd))
			installs = append(installs, fullCmd.Command.InstallProfile.Payload)
		case "RemoveProfile":
			require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd))
			removes = append(removes, fullCmd.Command.RemoveProfile.Identifier)
		}

		if forceDeviceErr {
			cmd, err = mdmDevice.Err(cmd.CommandUUID, []mdm.ErrorChain{})
		} else {
			cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID)
		}

		require.NoError(t, err)
	}

	return installs, removes
}

func setupExpectedFleetdProfile(t *testing.T, serverURL string, enrollSecret string, teamID *uint) []byte {
	var b bytes.Buffer
	params := mobileconfig.FleetdProfileOptions{
		EnrollSecret: enrollSecret,
		ServerURL:    serverURL,
		PayloadType:  mobileconfig.FleetdConfigPayloadIdentifier,
		PayloadName:  servermdm.FleetdConfigProfileName,
	}
	err := mobileconfig.FleetdProfileTemplate.Execute(&b, params)
	require.NoError(t, err)
	return b.Bytes()
}

func setupExpectedCAProfile(t *testing.T, ds *mysql.Datastore) []byte {
	assets, err := ds.GetAllMDMConfigAssetsByName(context.Background(), []fleet.MDMAssetName{
		fleet.MDMAssetCACert,
	}, nil)
	require.NoError(t, err)

	block, _ := pem.Decode(assets[fleet.MDMAssetCACert].Value)
	require.NotNil(t, block)
	require.Equal(t, "CERTIFICATE", block.Type)

	var b bytes.Buffer
	params := mobileconfig.FleetCARootTemplateOptions{
		PayloadName:       servermdm.FleetCAConfigProfileName,
		PayloadIdentifier: mobileconfig.FleetCARootConfigPayloadIdentifier,
		Certificate:       base64.StdEncoding.EncodeToString(block.Bytes),
	}
	err = mobileconfig.FleetCARootTemplate.Execute(&b, params)
	require.NoError(t, err)
	return b.Bytes()
}

func setupPusher(s *integrationMDMTestSuite, t *testing.T, mdmDevice *mdmtest.TestAppleMDMClient) {
	origPush := s.pushProvider.PushFunc
	s.pushProvider.PushFunc = func(_ context.Context, pushes []*mdm.Push) (map[string]*push.Response, error) {
		require.Len(t, pushes, 1)
		require.Equal(t, pushes[0].PushMagic, "pushmagic"+mdmDevice.SerialNumber)
		res := map[string]*push.Response{
			pushes[0].Token.String(): {
				Id:  uuid.New().String(),
				Err: nil,
			},
		}
		return res, nil
	}
	t.Cleanup(func() { s.pushProvider.PushFunc = origPush })
}

func createHostThenEnrollMDM(ds fleet.Datastore, fleetServerURL string, t *testing.T) (*fleet.Host, *mdmtest.TestAppleMDMClient) {
	fleetHost, err := ds.NewHost(context.Background(), &fleet.Host{
		DetailUpdatedAt: time.Now(),
		LabelUpdatedAt:  time.Now(),
		PolicyUpdatedAt: time.Now(),
		SeenTime:        time.Now().Add(-1 * time.Minute),
		OsqueryHostID:   ptr.String(t.Name() + uuid.New().String()),
		NodeKey:         ptr.String(t.Name() + uuid.New().String()),
		Hostname:        fmt.Sprintf("%sfoo.local", t.Name()),
		Platform:        "darwin",
		HardwareModel:   "MacBookPro16,1",

		UUID:           strings.ToUpper(uuid.NewString()),
		HardwareSerial: mdmtest.RandSerialNumber(),
	})
	require.NoError(t, err)
	mdmDevice := enrollMacOSHostInMDMManually(t, fleetHost, ds, fleetServerURL)

	return fleetHost, mdmDevice
}

func enrollMacOSHostInMDMManually(t *testing.T, host *fleet.Host, ds fleet.Datastore, fleetServerURL string) *mdmtest.TestAppleMDMClient {
	desktopToken := uuid.New().String()
	mdmDevice := mdmtest.NewTestMDMClientAppleDesktopManual(fleetServerURL, desktopToken)
	mdmDevice.UUID = host.UUID
	mdmDevice.SerialNumber = host.HardwareSerial

	err := ds.SetOrUpdateDeviceAuthToken(context.Background(), host.ID, desktopToken)
	require.NoError(t, err)

	err = mdmDevice.Enroll()
	require.NoError(t, err)
	return mdmDevice
}

func (s *integrationMDMTestSuite) createAppleMobileHostThenDEPEnrollMDM(platform string, serial string) (*fleet.Host, *mdmtest.TestAppleMDMClient) {
	ctx := context.Background()
	t := s.T()

	model := "iPhone14,6"
	if platform == "ipados" {
		model = "iPad13,18"
	}

	// create a host with minimal information and the serial, no uuid/osquery id
	// (as when created via DEP sync).
	dbZeroTime := time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)
	fleetHost, err := s.ds.NewHost(ctx, &fleet.Host{
		HardwareSerial:   serial,
		HardwareModel:    model,
		Platform:         platform,
		LastEnrolledAt:   dbZeroTime,
		DetailUpdatedAt:  time.Now(), // so that we don't trigger a cron detail update
		RefetchRequested: true,
	})
	require.NoError(t, err)
	require.Equal(t, dbZeroTime, fleetHost.LastEnrolledAt)

	depURLToken := loadEnrollmentProfileDEPToken(t, s.ds)
	mdmDevice := mdmtest.NewTestMDMClientAppleDEPFromDevice(s.server.URL, depURLToken, serial, model)
	mdmDevice.SerialNumber = serial
	err = mdmDevice.Enroll()
	require.NoError(t, err)

	return fleetHost, mdmDevice
}

func (s *integrationMDMTestSuite) createAppleMobileHostThenEnrollMDM(platform string) (*fleet.Host, *mdmtest.TestAppleMDMClient) {
	ctx := context.Background()
	t := s.T()

	model := "iPhone14,6"
	if platform == "ipados" {
		model = "iPad13,18"
	}

	// create a host with minimal information and the serial, no uuid/osquery id
	// (as when created via DEP sync).
	dbZeroTime := time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)
	serialNumber := mdmtest.RandSerialNumber()
	fleetHost, err := s.ds.NewHost(ctx, &fleet.Host{
		HardwareSerial:   serialNumber,
		HardwareModel:    model,
		Platform:         platform,
		LastEnrolledAt:   dbZeroTime,
		DetailUpdatedAt:  time.Now(), // so that we don't trigger a cron detail update
		RefetchRequested: true,
	})
	require.NoError(t, err)
	require.Equal(t, dbZeroTime, fleetHost.LastEnrolledAt)

	// Perform the MDM enrollment.
	mdmEnrollInfo := mdmtest.AppleEnrollInfo{
		SCEPChallenge: s.scepChallenge,
		SCEPURL:       s.server.URL + apple_mdm.SCEPPath,
		MDMURL:        s.server.URL + apple_mdm.MDMPath,
	}

	mdmDevice := mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo, model)
	mdmDevice.SerialNumber = serialNumber
	err = mdmDevice.Enroll()
	require.NoError(t, err)

	return fleetHost, mdmDevice
}

func createWindowsHostThenEnrollMDM(ds fleet.Datastore, fleetServerURL string, t *testing.T) (*fleet.Host, *mdmtest.TestWindowsMDMClient) {
	host := createOrbitEnrolledHost(t, "windows", uuid.NewString(), ds)
	mdmDevice := enrollWindowsHostInMDMViaOrbit(t, host, ds, fleetServerURL)
	return host, mdmDevice
}

func enrollWindowsHostInMDMViaOrbit(t *testing.T, host *fleet.Host, ds fleet.Datastore, fleetServerURL string) *mdmtest.TestWindowsMDMClient {
	mdmDevice := mdmtest.NewTestMDMClientWindowsProgramatic(fleetServerURL, *host.OrbitNodeKey)
	err := mdmDevice.Enroll()
	require.NoError(t, err)
	_, err = ds.UpdateMDMWindowsEnrollmentsHostUUID(context.Background(), host.UUID, mdmDevice.DeviceID)
	require.NoError(t, err)
	err = ds.SetOrUpdateMDMData(context.Background(), host.ID, false, true, fleetServerURL, false, fleet.WellKnownMDMFleet, "", false)
	require.NoError(t, err)
	return mdmDevice
}

// Simulates a host being orbit enrolled first then an MDM enrollment coming via the settings app
func createWindowsHostThenEnrollMDMViaSettingsApp(ds fleet.Datastore, fleetServerURL, email string, t *testing.T) (*fleet.Host, *mdmtest.TestWindowsMDMClient) {
	host := createOrbitEnrolledHost(t, "windows", uuid.NewString(), ds)
	mdmDevice := enrollWindowsMDMViaSettingsApp(t, ds, fleetServerURL, email)
	return host, mdmDevice
}

// Note that this method only creates the MDM Enrollment but it will still need to be linked to the host record either
// via DS methods or by simualting a refetch.
func enrollWindowsMDMViaSettingsApp(t *testing.T, ds fleet.Datastore, fleetServerURL, email string) *mdmtest.TestWindowsMDMClient {
	mdmDevice := mdmtest.NewTestMDMClientWindowsAutomatic(fleetServerURL, email, mdmtest.TestWindowsMDMClientNotInOOBE())
	err := mdmDevice.Enroll()
	require.NoError(t, err)
	return mdmDevice
}

func enrollWindowsHostInMDMViaAutopilot(t *testing.T, ds fleet.Datastore, fleetServerURL, email string) *mdmtest.TestWindowsMDMClient {
	mdmDevice := mdmtest.NewTestMDMClientWindowsAutomatic(fleetServerURL, email)
	err := mdmDevice.Enroll()
	require.NoError(t, err)
	return mdmDevice
}

// Simulates a host fetching queries then reporting the queries required for an Azure-initiated Windows host to
// link up to its MDM enrollment.
func (s *integrationMDMTestSuite) simulateMDMWindowsQueries(t *testing.T, host *fleet.Host, mdmDevice *mdmtest.TestWindowsMDMClient) {
	s.lq.On("QueriesForHost", host.ID).Return(map[string]string{fmt.Sprintf("%d", host.ID): "select 1 from osquery;"}, nil)
	req := getDistributedQueriesRequest{NodeKey: *host.NodeKey}
	var dqResp getDistributedQueriesResponse

	// Ensure we can read distributed queries for the host.
	err := s.ds.UpdateHostRefetchRequested(context.Background(), host.ID, true)
	require.NoError(t, err)

	s.DoJSON("POST", "/api/osquery/distributed/read", req, http.StatusOK, &dqResp)
	require.Contains(t, dqResp.Queries, "fleet_detail_query_mdm_device_id_windows")
	require.Contains(t, dqResp.Queries, "fleet_detail_query_mdm_windows")

	distributedReq := SubmitDistributedQueryResultsRequest{
		NodeKey: *host.NodeKey,
		Results: map[string][]map[string]string{
			"fleet_detail_query_mdm_windows": {
				{"aad_resource_id": s.server.URL, "discovery_service_url": s.server.URL + "/api/mdm/microsoft/discovery", "provider_id": "Fleet", "installation_type": "Client"},
			},
			"fleet_detail_query_mdm_device_id_windows": {
				{"name": "DeviceClientId", "data": mdmDevice.DeviceID},
			},
		},
		Statuses: map[string]fleet.OsqueryStatus{
			"fleet_detail_query_mdm_device_id_windows": 0,
			"fleet_detail_query_mdm_windows":           0,
		},
	}
	distributedResp := submitDistributedQueryResultsResponse{}
	s.DoJSON("POST", "/api/osquery/distributed/write", distributedReq, http.StatusOK, &distributedResp)
}

func loadEnrollmentProfileDEPToken(t *testing.T, ds *mysql.Datastore) string {
	var token string
	mysql.ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
		return sqlx.GetContext(context.Background(), q, &token,
			`SELECT token FROM mdm_apple_enrollment_profiles`)
	})
	return token
}

func (s *integrationMDMTestSuite) TestDeviceMDMManualEnroll() {
	t := s.T()

	token := "token_test_manual_enroll"
	createHostAndDeviceToken(t, s.ds, token)

	// invalid token fails
	s.DoRaw("GET", "/api/latest/fleet/device/invalid_token/mdm/apple/manual_enrollment_profile", nil, http.StatusUnauthorized)

	// valid token downloads the profile
	var r1 getDeviceMDMManualEnrollProfileResponse
	s.DoJSON("GET", "/api/latest/fleet/device/"+token+"/mdm/apple/manual_enrollment_profile", nil, http.StatusOK, &r1)
	require.NotEmpty(t, r1.EnrollURL)
	require.Contains(t, r1.EnrollURL, s.server.URL+"/enroll?enroll_secret=")
	u, err := url.Parse(r1.EnrollURL)
	require.NoError(t, err)
	q, err := url.ParseQuery(u.RawQuery)
	require.NoError(t, err)
	require.NotEmpty(t, q.Get("enroll_secret"))

	// set end user auth enabled
	appConfig, err := s.ds.AppConfig(t.Context())
	require.NoError(t, err)
	appConfig.MDM.MacOSSetup.EnableEndUserAuthentication = true

	err = s.ds.SaveAppConfig(t.Context(), appConfig)
	require.NoError(t, err)

	// works with end user auth enabled
	var r2 getDeviceMDMManualEnrollProfileResponse
	s.DoJSON("GET", "/api/latest/fleet/device/"+token+"/mdm/apple/manual_enrollment_profile", nil, http.StatusOK, &r2)
	require.NotEmpty(t, r2.EnrollURL)
	require.Contains(t, r2.EnrollURL, s.server.URL+"/enroll?enroll_secret=")
	u2, err := url.Parse(r2.EnrollURL)
	require.NoError(t, err)
	q2, err := url.ParseQuery(u2.RawQuery)
	require.NoError(t, err)
	require.NotEmpty(t, q2.Get("enroll_secret"))
}

func (s *integrationMDMTestSuite) TestAppleMDMDeviceEnrollment() {
	t := s.T()
	// Clear out all activities to other test leaking activities into this one.
	mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
		_, err := q.ExecContext(context.Background(), `DELETE FROM activities`)
		return err
	})

	// Enroll two devices into MDM
	mdmEnrollInfo := mdmtest.AppleEnrollInfo{
		SCEPChallenge: s.scepChallenge,
		SCEPURL:       s.server.URL + apple_mdm.SCEPPath,
		MDMURL:        s.server.URL + apple_mdm.MDMPath,
	}
	mdmDeviceA := mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo, "MacBookPro16,1")
	err := mdmDeviceA.Enroll()
	require.NoError(t, err)
	s.lastActivityOfTypeMatches(fleet.ActivityTypeMDMEnrolled{}.ActivityName(),
		fmt.Sprintf(`{"host_serial": "%s", "enrollment_id": null, "host_display_name": "%s (%s)", "installed_from_dep": false, "mdm_platform": "apple", "platform": "darwin"}`, mdmDeviceA.SerialNumber, mdmDeviceA.Model, mdmDeviceA.SerialNumber), 0)
	mdmDeviceB := mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo, "MacBookPro16,1")
	err = mdmDeviceB.Enroll()
	require.NoError(t, err)
	s.lastActivityOfTypeMatches(fleet.ActivityTypeMDMEnrolled{}.ActivityName(),
		fmt.Sprintf(`{"host_serial": "%s", "enrollment_id": null, "host_display_name": "%s (%s)", "installed_from_dep": false, "mdm_platform": "apple", "platform": "darwin"}`, mdmDeviceB.SerialNumber, mdmDeviceB.Model, mdmDeviceB.SerialNumber), 0)
	// Find the ID of Fleet's MDM solution
	var mdmID uint
	mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
		return sqlx.GetContext(context.Background(), q, &mdmID,
			`SELECT id FROM mobile_device_management_solutions WHERE name = ?`, fleet.WellKnownMDMFleet)
	})

	// Check that both devices are returned by the /hosts endpoint
	listHostsRes := listHostsResponse{}
	s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listHostsRes, "mdm_id", fmt.Sprint(mdmID))
	require.Len(t, listHostsRes.Hosts, 2)
	require.EqualValues(
		t,
		[]string{mdmDeviceA.UUID, mdmDeviceB.UUID},
		[]string{listHostsRes.Hosts[0].UUID, listHostsRes.Hosts[1].UUID},
	)

	var targetHostID uint
	var lastEnroll time.Time
	for _, host := range listHostsRes.Hosts {
		if host.UUID == mdmDeviceA.UUID {
			targetHostID = host.ID
			lastEnroll = host.LastEnrolledAt
			break
		}
	}

	// set an enroll secret
	var applyResp applyEnrollSecretSpecResponse
	s.DoJSON("POST", "/api/latest/fleet/spec/enroll_secret", applyEnrollSecretSpecRequest{
		Spec: &fleet.EnrollSecretSpec{
			Secrets: []*fleet.EnrollSecret{{Secret: t.Name()}},
		},
	}, http.StatusOK, &applyResp)

	// simulate a matching host enrolling via osquery
	j, err := json.Marshal(&contract.EnrollOsqueryAgentRequest{
		EnrollSecret:   t.Name(),
		HostIdentifier: mdmDeviceA.UUID,
	})
	require.NoError(t, err)
	var enrollResp contract.EnrollOsqueryAgentResponse
	hres := s.DoRawNoAuth("POST", "/api/osquery/enroll", j, http.StatusOK)
	defer hres.Body.Close()
	require.NoError(t, json.NewDecoder(hres.Body).Decode(&enrollResp))
	require.NotEmpty(t, enrollResp.NodeKey)

	// query all hosts
	listHostsRes = listHostsResponse{}
	s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listHostsRes)
	// we still have only two hosts
	require.Len(t, listHostsRes.Hosts, 2)

	// LastEnrolledAt should have been updated
	var getHostResp getHostResponse
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", targetHostID), nil, http.StatusOK, &getHostResp)
	require.Greater(t, getHostResp.Host.LastEnrolledAt, lastEnroll)

	// Unenroll a device
	err = mdmDeviceA.Checkout()
	require.NoError(t, err)

	// An activity is created
	activities := listActivitiesResponse{}
	s.DoJSON("GET", "/api/latest/fleet/activities", nil, http.StatusOK, &activities)

	found := false
	for _, activity := range activities.Activities {
		if activity.Type == "mdm_unenrolled" {
			found = true
			require.Nil(t, activity.ActorID)
			require.Nil(t, activity.ActorFullName)
			require.JSONEq(t, fmt.Sprintf(`{"enrollment_id": null, "host_serial": "%s", "host_display_name": "%s (%s)", "installed_from_dep": false, "platform": "darwin"}`, mdmDeviceA.SerialNumber, mdmDeviceA.Model, mdmDeviceA.SerialNumber), string(*activity.Details))
		}
	}
	require.True(t, found)
}

func (s *integrationMDMTestSuite) TestDeviceMultipleAuthMessages() {
	t := s.T()

	mdmDevice := mdmtest.NewTestMDMClientAppleDirect(mdmtest.AppleEnrollInfo{
		SCEPChallenge: s.scepChallenge,
		SCEPURL:       s.server.URL + apple_mdm.SCEPPath,
		MDMURL:        s.server.URL + apple_mdm.MDMPath,
	}, "MacBookPro16,1")
	err := mdmDevice.Enroll()
	require.NoError(t, err)

	listHostsRes := listHostsResponse{}
	s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listHostsRes)
	require.Len(s.T(), listHostsRes.Hosts, 1)

	// send the auth message again, we still have only one host
	err = mdmDevice.Authenticate()
	require.NoError(t, err)
	listHostsRes = listHostsResponse{}
	s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listHostsRes)
	require.Len(s.T(), listHostsRes.Hosts, 1)
}

func (s *integrationMDMTestSuite) TestAppleMDMCSRRequest() {
	t := s.T()

	var errResp validationErrResp
	// missing arguments
	s.DoJSON("POST", "/api/latest/fleet/mdm/apple/request_csr", requestMDMAppleCSRRequest{}, http.StatusUnprocessableEntity, &errResp)
	require.Len(t, errResp.Errors, 1)
	require.Equal(t, errResp.Errors[0].Name, "email_address")

	// invalid email address
	errResp = validationErrResp{}
	s.DoJSON("POST", "/api/latest/fleet/mdm/apple/request_csr", requestMDMAppleCSRRequest{EmailAddress: "abc", Organization: "def"}, http.StatusUnprocessableEntity, &errResp)
	require.Len(t, errResp.Errors, 1)
	require.Equal(t, errResp.Errors[0].Name, "email_address")

	// missing organization
	errResp = validationErrResp{}
	s.DoJSON("POST", "/api/latest/fleet/mdm/apple/request_csr", requestMDMAppleCSRRequest{EmailAddress: "a@b.c", Organization: ""}, http.StatusUnprocessableEntity, &errResp)
	require.Len(t, errResp.Errors, 1)
	require.Equal(t, errResp.Errors[0].Name, "organization")

	// fleetdm CSR request failed
	s.FailNextCSRRequestWith(http.StatusBadRequest)
	errResp = validationErrResp{}
	s.DoJSON("POST", "/api/latest/fleet/mdm/apple/request_csr", requestMDMAppleCSRRequest{EmailAddress: "a@b.c", Organization: "test"}, http.StatusUnprocessableEntity, &errResp)
	require.Len(t, errResp.Errors, 1)
	require.Contains(t, errResp.Errors[0].Reason, "this email address is not valid")

	s.FailNextCSRRequestWith(http.StatusInternalServerError)
	errResp = validationErrResp{}
	s.DoJSON("POST", "/api/latest/fleet/mdm/apple/request_csr", requestMDMAppleCSRRequest{EmailAddress: "a@b.c", Organization: "test"}, http.StatusBadGateway, &errResp)
	require.Len(t, errResp.Errors, 1)
	require.Contains(t, errResp.Errors[0].Reason, "FleetDM CSR request failed")

	var reqCSRResp requestMDMAppleCSRResponse
	// fleetdm CSR request succeeds
	s.SucceedNextCSRRequest()
	s.DoJSON("POST", "/api/latest/fleet/mdm/apple/request_csr", requestMDMAppleCSRRequest{EmailAddress: "a@b.c", Organization: "test"}, http.StatusOK, &reqCSRResp)
	require.Contains(t, string(reqCSRResp.APNsKey), "-----BEGIN RSA PRIVATE KEY-----\n")
	require.Contains(t, string(reqCSRResp.SCEPCert), "-----BEGIN CERTIFICATE-----\n")
	require.Contains(t, string(reqCSRResp.SCEPKey), "-----BEGIN RSA PRIVATE KEY-----\n")
}

func (s *integrationMDMTestSuite) TestGetMDMCSR() {
	t := s.T()
	ctx := context.Background()

	// Validate errors if no private key is set
	testSetEmptyPrivateKey = true
	t.Cleanup(func() { testSetEmptyPrivateKey = false })
	s.uploadDataViaForm("/api/latest/fleet/mdm/apple/apns_certificate", "certificate", "certificate.pem", []byte("-----BEGIN CERTIFICATE-----\nZm9vCg==\n-----END CERTIFICATE-----"), http.StatusInternalServerError, "Couldn't add APNs certificate. Missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key", nil)

	r := s.Do("GET", "/api/latest/fleet/mdm/apple/request_csr", getMDMAppleCSRRequest{}, http.StatusInternalServerError)
	require.Contains(t, extractServerErrorText(r.Body), "Couldn't download signed CSR. Missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key")
	testSetEmptyPrivateKey = false

	// ensure we leave everything in a clean state for other tests
	t.Cleanup(s.appleCoreCertsSetup)

	// Delete APNS cert, should soft delete all certs and keys created in this test
	s.Do("DELETE", "/api/latest/fleet/mdm/apple/apns_certificate", nil, http.StatusOK)

	assets, err := s.ds.GetAllMDMConfigAssetsByName(ctx,
		[]fleet.MDMAssetName{fleet.MDMAssetCACert, fleet.MDMAssetCAKey, fleet.MDMAssetAPNSKey, fleet.MDMAssetAPNSCert}, nil)
	var nfe fleet.NotFoundError
	require.ErrorAs(t, err, &nfe)
	require.Nil(t, assets)

	// trying to upload a certificate without generating a private key first is not allowed
	s.uploadDataViaForm("/api/latest/fleet/mdm/apple/apns_certificate", "certificate", "certificate.pem", []byte("-----BEGIN CERTIFICATE-----\nZm9vCg==\n-----END CERTIFICATE-----"), http.StatusBadRequest, "Please generate a private key first.", nil)

	// Check that we return bad gateway if the website API errors
	s.FailNextCSRRequestWith(http.StatusInternalServerError)
	errResp := validationErrResp{}
	s.DoJSON("GET", "/api/latest/fleet/mdm/apple/request_csr", getMDMAppleCSRRequest{}, http.StatusBadGateway, &errResp)
	require.Len(t, errResp.Errors, 1)
	require.Contains(t, errResp.Errors[0].Reason, "FleetDM CSR request failed")

	// Check that we return bad request if the website API does (it will do this in case of an
	// invalid email address
	s.FailNextCSRRequestWith(http.StatusUnprocessableEntity)
	errResp = validationErrResp{}
	s.DoJSON("GET", "/api/latest/fleet/mdm/apple/request_csr", getMDMAppleCSRRequest{}, http.StatusUnprocessableEntity, &errResp)
	require.Len(t, errResp.Errors, 1)
	require.Contains(t, errResp.Errors[0].Reason, "this email address is not valid")

	// Invalid APNS cert upload attempt
	s.uploadDataViaForm("/api/latest/fleet/mdm/apple/apns_certificate", "certificate", "certificate.pem", []byte("invalid-cert"), http.StatusUnprocessableEntity, "Invalid certificate. Please provide a valid certificate from Apple Push Certificate Portal.", nil)

	// simulate a renew flow
	s.appleCoreCertsSetup()
}

func (s *integrationMDMTestSuite) uploadDataViaForm(endpoint, fieldName, fileName string, data []byte, expectedStatus int, wantErr string, response any) {
	s.uploadDataViaFormWithVerb(endpoint, "POST", fieldName, fileName, data, expectedStatus, wantErr, response)
}

func (s *integrationMDMTestSuite) uploadDataViaFormWithVerb(endpoint, verb, fieldName, fileName string, data []byte, expectedStatus int, wantErr string, response any) {
	t := s.T()

	var b bytes.Buffer
	w := multipart.NewWriter(&b)

	// add the package field
	fw, err := w.CreateFormFile(fieldName, fileName)
	require.NoError(t, err)
	_, err = io.Copy(fw, bytes.NewBuffer(data))
	require.NoError(t, err)

	w.Close()

	headers := map[string]string{
		"Content-Type":  w.FormDataContentType(),
		"Accept":        "application/json",
		"Authorization": fmt.Sprintf("Bearer %s", s.token),
	}

	res := s.DoRawWithHeaders(verb, endpoint, b.Bytes(), expectedStatus, headers)
	if wantErr != "" {
		errMsg := extractServerErrorText(res.Body)
		assert.Contains(t, errMsg, wantErr)
	}

	if response != nil {
		err := json.NewDecoder(res.Body).Decode(&response)
		require.NoError(t, err)
	}
}

func (s *integrationMDMTestSuite) TestMDMAppleUnenroll() {
	t := s.T()

	// Enroll a device into MDM.
	mdmDevice := mdmtest.NewTestMDMClientAppleDirect(mdmtest.AppleEnrollInfo{
		SCEPChallenge: s.scepChallenge,
		SCEPURL:       s.server.URL + apple_mdm.SCEPPath,
		MDMURL:        s.server.URL + apple_mdm.MDMPath,
	}, "MacBookPro16,1")
	err := mdmDevice.Enroll()
	require.NoError(t, err)

	// set an enroll secret
	var applyResp applyEnrollSecretSpecResponse
	s.DoJSON("POST", "/api/latest/fleet/spec/enroll_secret", applyEnrollSecretSpecRequest{
		Spec: &fleet.EnrollSecretSpec{
			Secrets: []*fleet.EnrollSecret{{Secret: t.Name()}},
		},
	}, http.StatusOK, &applyResp)

	// simulate a matching host enrolling via osquery
	j, err := json.Marshal(&contract.EnrollOsqueryAgentRequest{
		EnrollSecret:   t.Name(),
		HostIdentifier: mdmDevice.UUID,
	})
	require.NoError(t, err)
	var enrollResp contract.EnrollOsqueryAgentResponse
	hres := s.DoRawNoAuth("POST", "/api/osquery/enroll", j, http.StatusOK)
	defer hres.Body.Close()
	require.NoError(t, json.NewDecoder(hres.Body).Decode(&enrollResp))
	require.NotEmpty(t, enrollResp.NodeKey)

	listHostsRes := listHostsResponse{}
	s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listHostsRes)
	require.Len(t, listHostsRes.Hosts, 1)
	h := listHostsRes.Hosts[0]

	// assign profiles to the host
	s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: [][]byte{
		mobileconfigForTest("N1", "I1"),
		mobileconfigForTest("N2", "I2"),
		mobileconfigForTest("N3", "I3"),
	}}, http.StatusNoContent)

	// trigger a sync and verify that there are profiles assigned to the host
	s.awaitTriggerProfileSchedule(t)

	var hostResp getHostResponse
	s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/hosts/%d", h.ID), getHostRequest{}, http.StatusOK, &hostResp)
	// 3 profiles added + 1 profile with fleetd configuration + 1 root CA config
	require.Len(t, *hostResp.Host.MDM.Profiles, 5)

	// returns success, but this is effectively a no-op because the host isn't enrolled yet.
	s.Do("DELETE", fmt.Sprintf("/api/latest/fleet/hosts/%d/mdm", h.ID), nil, http.StatusNoContent)

	// we're going to modify this mock, make sure we restore its default
	originalPushMock := s.pushProvider.PushFunc
	defer func() { s.pushProvider.PushFunc = originalPushMock }()

	// if there's an error coming from APNs servers
	s.pushProvider.PushFunc = func(_ context.Context, pushes []*mdm.Push) (map[string]*push.Response, error) {
		return map[string]*push.Response{
			pushes[0].Token.String(): {
				Id:  uuid.New().String(),
				Err: errors.New("test"),
			},
		}, nil
	}
	s.Do("DELETE", fmt.Sprintf("/api/latest/fleet/hosts/%d/mdm", h.ID), nil, http.StatusBadGateway)

	// if there was an error unrelated to APNs
	s.pushProvider.PushFunc = func(_ context.Context, pushes []*mdm.Push) (map[string]*push.Response, error) {
		res := map[string]*push.Response{
			pushes[0].Token.String(): {
				Id:  uuid.New().String(),
				Err: nil,
			},
		}
		return res, errors.New("baz")
	}
	s.Do("DELETE", fmt.Sprintf("/api/latest/fleet/hosts/%d/mdm", h.ID), nil, http.StatusInternalServerError)

	// try again, but this time the host is online and answers
	var checkoutErr error
	s.pushProvider.PushFunc = func(ctx context.Context, pushes []*mdm.Push) (map[string]*push.Response, error) {
		res, err := mockSuccessfulPush(ctx, pushes)
		checkoutErr = mdmDevice.Checkout()
		return res, err
	}
	s.Do("DELETE", fmt.Sprintf("/api/latest/fleet/hosts/%d/mdm", h.ID), nil, http.StatusNoContent)
	// trying again fails with 409 as it is alreayd unenrolled
	s.Do("DELETE", fmt.Sprintf("/api/latest/fleet/hosts/%d/mdm", h.ID), nil, http.StatusConflict)

	require.NoError(t, checkoutErr)

	// profiles are removed and the host is no longer enrolled
	hostResp = getHostResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/hosts/%d", h.ID), getHostRequest{}, http.StatusOK, &hostResp)
	require.Nil(t, hostResp.Host.MDM.Profiles)
	require.Equal(t, "", hostResp.Host.MDM.Name)
}

func (s *integrationMDMTestSuite) TestMDMDiskEncryptionSettingBackwardsCompat() {
	t := s.T()

	acResp := appConfigResponse{}
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
		"mdm": { "enable_disk_encryption": false }
  }`), http.StatusOK, &acResp)
	assert.False(t, acResp.MDM.EnableDiskEncryption.Value)

	// new config takes precedence over old config
	acResp = appConfigResponse{}
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
	  "mdm": { "enable_disk_encryption": false, "macos_settings": {"enable_disk_encryption": true} }
  }`), http.StatusOK, &acResp)
	assert.False(t, acResp.MDM.EnableDiskEncryption.Value)

	s.assertConfigProfilesByIdentifier(nil, mobileconfig.FleetFileVaultPayloadIdentifier, false)

	// if new config is not present, old config is applied
	acResp = appConfigResponse{}
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
	  "mdm": { "macos_settings": {"enable_disk_encryption": true} }
  }`), http.StatusOK, &acResp)
	assert.True(t, acResp.MDM.EnableDiskEncryption.Value)
	s.assertConfigProfilesByIdentifier(nil, mobileconfig.FleetFileVaultPayloadIdentifier, true)

	// new config takes precedence over old config again
	acResp = appConfigResponse{}
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
	  "mdm": { "enable_disk_encryption": false, "macos_settings": {"enable_disk_encryption": true} }
  }`), http.StatusOK, &acResp)
	assert.False(t, acResp.MDM.EnableDiskEncryption.Value)
	s.assertConfigProfilesByIdentifier(nil, mobileconfig.FleetFileVaultPayloadIdentifier, false)

	// unrelated change doesn't affect the disk encryption setting
	acResp = appConfigResponse{}
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
	  "mdm": { "macos_settings": {"custom_settings": ["test.mobileconfig"]} }
  }`), http.StatusOK, &acResp)
	assert.False(t, acResp.MDM.EnableDiskEncryption.Value)

	// Same tests, but for teams
	team, err := s.ds.NewTeam(context.Background(), &fleet.Team{
		Name:        "team1_" + t.Name(),
		Description: "desc team1_" + t.Name(),
	})
	require.NoError(t, err)

	checkTeamDiskEncryption := func(wantSetting bool) {
		var teamResp getTeamResponse
		s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), nil, http.StatusOK, &teamResp)
		require.Equal(t, wantSetting, teamResp.Team.Config.MDM.EnableDiskEncryption)
	}

	// after creation, disk encryption is off
	checkTeamDiskEncryption(false)

	// new config takes precedence over old config
	teamSpecs := applyTeamSpecsRequest{Specs: []*fleet.TeamSpec{{
		Name: team.Name,
		MDM: fleet.TeamSpecMDM{
			EnableDiskEncryption: optjson.SetBool(false),
			MacOSSettings:        map[string]interface{}{"enable_disk_encryption": true},
		},
	}}}
	s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK)
	checkTeamDiskEncryption(false)
	s.assertConfigProfilesByIdentifier(ptr.Uint(team.ID), mobileconfig.FleetFileVaultPayloadIdentifier, false)

	// if new config is not present, old config is applied
	teamSpecs = applyTeamSpecsRequest{Specs: []*fleet.TeamSpec{{
		Name: team.Name,
		MDM: fleet.TeamSpecMDM{
			MacOSSettings: map[string]interface{}{"enable_disk_encryption": true},
		},
	}}}
	s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK)
	checkTeamDiskEncryption(true)
	s.assertConfigProfilesByIdentifier(ptr.Uint(team.ID), mobileconfig.FleetFileVaultPayloadIdentifier, true)

	// new config takes precedence over old config again
	teamSpecs = applyTeamSpecsRequest{Specs: []*fleet.TeamSpec{{
		Name: team.Name,
		MDM: fleet.TeamSpecMDM{
			EnableDiskEncryption: optjson.SetBool(false),
			MacOSSettings:        map[string]interface{}{"enable_disk_encryption": true},
		},
	}}}
	s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK)
	checkTeamDiskEncryption(false)
	s.assertConfigProfilesByIdentifier(ptr.Uint(team.ID), mobileconfig.FleetFileVaultPayloadIdentifier, false)

	// unrelated change doesn't affect the disk encryption setting
	teamSpecs = applyTeamSpecsRequest{Specs: []*fleet.TeamSpec{{
		Name: team.Name,
		MDM: fleet.TeamSpecMDM{
			EnableDiskEncryption: optjson.SetBool(false),
			MacOSSettings:        map[string]interface{}{"custom_settings": []interface{}{"A", "B"}},
		},
	}}}
	s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK)
	checkTeamDiskEncryption(false)
	s.assertConfigProfilesByIdentifier(ptr.Uint(team.ID), mobileconfig.FleetFileVaultPayloadIdentifier, false)
}

func (s *integrationMDMTestSuite) TestDiskEncryptionSharedSetting() {
	t := s.T()

	// create a team
	teamName := t.Name()
	team := &fleet.Team{
		Name:        teamName,
		Description: "desc " + teamName,
	}
	var createTeamResp teamResponse
	s.DoJSON("POST", "/api/latest/fleet/teams", team, http.StatusOK, &createTeamResp)
	require.NotZero(t, createTeamResp.Team.ID)

	setMDMEnabled := func(macMDM, windowsMDM bool) {
		appConf, err := s.ds.AppConfig(context.Background())
		require.NoError(s.T(), err)
		appConf.MDM.WindowsEnabledAndConfigured = windowsMDM
		appConf.MDM.EnabledAndConfigured = macMDM
		err = s.ds.SaveAppConfig(context.Background(), appConf)
		require.NoError(s.T(), err)
	}

	// before doing any modifications, grab the current values and make
	// sure they're set to the same ones on cleanup to not interfere with
	// other tests.
	origAppConf, err := s.ds.AppConfig(context.Background())
	require.NoError(s.T(), err)
	t.Cleanup(func() {
		err := s.ds.SaveAppConfig(context.Background(), origAppConf)
		require.NoError(s.T(), err)
	})

	checkConfigSetSucceeds := func() {
		res := s.Do("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
		"mdm": { "enable_disk_encryption": true }
  }`), http.StatusOK)
		errMsg := extractServerErrorText(res.Body)
		require.Empty(t, errMsg)

		// try to create a new team using specs
		teamSpecs := map[string]any{
			"specs": []any{
				map[string]any{
					"name": teamName + uuid.NewString(),
					"mdm": map[string]any{
						"enable_disk_encryption": true,
					},
				},
			},
		}
		res = s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK)
		errMsg = extractServerErrorText(res.Body)
		require.Empty(t, errMsg)

		// edit the existing team using specs
		teamSpecs = map[string]any{
			"specs": []any{
				map[string]any{
					"name": teamName,
					"mdm": map[string]any{
						"enable_disk_encryption": true,
					},
				},
			},
		}
		res = s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK)
		errMsg = extractServerErrorText(res.Body)
		require.Empty(t, errMsg)

		// always try to set the value to `false` so we start fresh
		s.Do("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
		"mdm": { "enable_disk_encryption": false }
  }`), http.StatusOK)
		teamSpecs = map[string]any{
			"specs": []any{
				map[string]any{
					"name": teamName,
					"mdm": map[string]any{
						"enable_disk_encryption": false,
					},
				},
			},
		}
		s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK)
	}

	// MDM config succeeds because we have a private key baked into default suite config
	setMDMEnabled(false, false)
	checkConfigSetSucceeds()

	// enable windows mdm, no errors
	setMDMEnabled(false, true)
	checkConfigSetSucceeds()

	// enable mac mdm, no errors
	setMDMEnabled(true, true)
	checkConfigSetSucceeds()

	// only macos mdm enabled, no errors
	setMDMEnabled(true, false)
	checkConfigSetSucceeds()
}

func (s *integrationMDMTestSuite) TestEscrowBuddyBackwardsCompat() {
	t := s.T()
	ctx := context.Background()

	// create a host
	host, _ := createHostThenEnrollMDM(s.ds, s.server.URL, t)
	orbitKey := setOrbitEnrollment(t, host, s.ds)
	host.OrbitNodeKey = &orbitKey

	// install a filevault profile for that host
	acResp := appConfigResponse{}
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
		"mdm": { "enable_disk_encryption": true }
  }`), http.StatusOK, &acResp)
	assert.True(t, acResp.MDM.EnableDiskEncryption.Value)

	// set the status as non-decryptable so a notification should be sent
	_, err := s.ds.SetOrUpdateHostDiskEncryptionKey(ctx, host, "", "", ptr.Bool(false))
	require.NoError(t, err)

	// notification is false because the escrow buddy capability is not set
	orbitConfigResp := orbitGetConfigResponse{}
	s.DoJSON("POST", "/api/fleet/orbit/config", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *host.OrbitNodeKey)), http.StatusOK, &orbitConfigResp)
	require.False(t, orbitConfigResp.Notifications.RotateDiskEncryptionKey)

	// send the request again, this time with the right header
	orbitConfigResp = orbitGetConfigResponse{}
	res := s.DoRawWithHeaders("POST", "/api/fleet/orbit/config", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *host.OrbitNodeKey)), http.StatusOK, map[string]string{
		"Authorization":          fmt.Sprintf("Bearer %s", s.token),
		fleet.CapabilitiesHeader: string(fleet.CapabilityEscrowBuddy),
	})
	err = json.NewDecoder(res.Body).Decode(&orbitConfigResp)
	require.NoError(t, err)
	require.True(t, orbitConfigResp.Notifications.RotateDiskEncryptionKey)
}

func (s *integrationMDMTestSuite) TestMDMAppleHostDiskEncryption() {
	t := s.T()
	ctx := context.Background()

	// create a host
	host, err := s.ds.NewHost(ctx, &fleet.Host{
		DetailUpdatedAt: time.Now(),
		LabelUpdatedAt:  time.Now(),
		PolicyUpdatedAt: time.Now(),
		SeenTime:        time.Now().Add(-1 * time.Minute),
		OsqueryHostID:   ptr.String(t.Name()),
		NodeKey:         ptr.String(t.Name()),
		UUID:            uuid.New().String(),
		Hostname:        fmt.Sprintf("%sfoo.local", t.Name()),
		Platform:        "darwin",
	})
	require.NoError(t, err)

	// install a filevault profile for that host

	acResp := appConfigResponse{}
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
		"mdm": { "enable_disk_encryption": true }
  }`), http.StatusOK, &acResp)
	assert.True(t, acResp.MDM.EnableDiskEncryption.Value)
	fileVaultProf := s.assertConfigProfilesByIdentifier(nil, mobileconfig.FleetFileVaultPayloadIdentifier, true)
	hostCmdUUID := uuid.New().String()
	err = s.ds.BulkUpsertMDMAppleHostProfiles(ctx, []*fleet.MDMAppleBulkUpsertHostProfilePayload{
		{
			ProfileUUID:       fileVaultProf.ProfileUUID,
			ProfileIdentifier: fileVaultProf.Identifier,
			HostUUID:          host.UUID,
			CommandUUID:       hostCmdUUID,
			OperationType:     fleet.MDMOperationTypeInstall,
			Status:            &fleet.MDMDeliveryPending,
			Checksum:          []byte("csum"),
			Scope:             fleet.PayloadScopeSystem,
		},
	})
	require.NoError(t, err)

	t.Cleanup(func() {
		err := s.ds.UpdateOrDeleteHostMDMAppleProfile(ctx, &fleet.HostMDMAppleProfile{
			HostUUID:      host.UUID,
			CommandUUID:   hostCmdUUID,
			ProfileUUID:   fileVaultProf.ProfileUUID,
			Status:        &fleet.MDMDeliveryVerifying,
			OperationType: fleet.MDMOperationTypeRemove,
		})
		require.NoError(t, err)
		// not an error if the profile does not exist
		_ = s.ds.DeleteMDMAppleConfigProfile(ctx, fileVaultProf.ProfileUUID)
	})

	// get that host - it should
	// report "enforcing" disk encryption
	getHostResp := getHostResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &getHostResp)
	require.NotNil(t, getHostResp.Host.MDM.MacOSSettings.DiskEncryption)
	require.Equal(t, fleet.DiskEncryptionEnforcing, *getHostResp.Host.MDM.MacOSSettings.DiskEncryption)
	require.Nil(t, getHostResp.Host.MDM.MacOSSettings.ActionRequired)
	require.NotNil(t, getHostResp.Host.MDM.OSSettings)
	require.NotNil(t, getHostResp.Host.MDM.OSSettings.DiskEncryption.Status)
	require.Equal(t, fleet.DiskEncryptionEnforcing, *getHostResp.Host.MDM.OSSettings.DiskEncryption.Status)
	require.Equal(t, "", getHostResp.Host.MDM.OSSettings.DiskEncryption.Detail)

	// report a profile install error
	err = s.ds.UpdateOrDeleteHostMDMAppleProfile(ctx, &fleet.HostMDMAppleProfile{
		HostUUID:      host.UUID,
		CommandUUID:   hostCmdUUID,
		ProfileUUID:   fileVaultProf.ProfileUUID,
		Status:        &fleet.MDMDeliveryFailed,
		OperationType: fleet.MDMOperationTypeInstall,
		Detail:        "test error",
	})
	require.NoError(t, err)

	// get that host - it should report "failed" disk encryption and include the error message detail
	getHostResp = getHostResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &getHostResp)
	require.NotNil(t, getHostResp.Host.MDM.MacOSSettings.DiskEncryption)
	require.Equal(t, fleet.DiskEncryptionFailed, *getHostResp.Host.MDM.MacOSSettings.DiskEncryption)
	require.Nil(t, getHostResp.Host.MDM.MacOSSettings.ActionRequired)
	require.NotNil(t, getHostResp.Host.MDM.OSSettings)
	require.NotNil(t, getHostResp.Host.MDM.OSSettings.DiskEncryption.Status)
	require.Equal(t, fleet.DiskEncryptionFailed, *getHostResp.Host.MDM.OSSettings.DiskEncryption.Status)
	require.Equal(t, "test error", getHostResp.Host.MDM.OSSettings.DiskEncryption.Detail)

	// report that the profile was installed and verified
	err = s.ds.UpdateOrDeleteHostMDMAppleProfile(ctx, &fleet.HostMDMAppleProfile{
		HostUUID:      host.UUID,
		CommandUUID:   hostCmdUUID,
		ProfileUUID:   fileVaultProf.ProfileUUID,
		Status:        &fleet.MDMDeliveryVerified,
		OperationType: fleet.MDMOperationTypeInstall,
		Detail:        "",
	})
	require.NoError(t, err)

	// get that host - it has no encryption key at this point, so it should
	// report "action_required" disk encryption and "log_out" action.
	getHostResp = getHostResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &getHostResp)
	require.NotNil(t, getHostResp.Host.MDM.MacOSSettings.DiskEncryption)
	require.Equal(t, fleet.DiskEncryptionActionRequired, *getHostResp.Host.MDM.MacOSSettings.DiskEncryption)
	require.NotNil(t, getHostResp.Host.MDM.MacOSSettings.ActionRequired)
	require.Equal(t, fleet.ActionRequiredRotateKey, *getHostResp.Host.MDM.MacOSSettings.ActionRequired)
	require.NotNil(t, getHostResp.Host.MDM.OSSettings)
	require.NotNil(t, getHostResp.Host.MDM.OSSettings.DiskEncryption.Status)
	require.Equal(t, fleet.DiskEncryptionActionRequired, *getHostResp.Host.MDM.OSSettings.DiskEncryption.Status)
	require.Equal(t, "", getHostResp.Host.MDM.OSSettings.DiskEncryption.Detail)

	// add an encryption key for the host
	assets, err := s.ds.GetAllMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{
		fleet.MDMAssetCACert,
	}, nil)
	require.NoError(t, err)

	block, _ := pem.Decode(assets[fleet.MDMAssetCACert].Value)
	require.NotNil(t, block)

	parsed, err := x509.ParseCertificate(block.Bytes)
	require.NoError(t, err)
	recoveryKey := "AAA-BBB-CCC"
	encryptedKey, err := pkcs7.Encrypt([]byte(recoveryKey), []*x509.Certificate{parsed})
	require.NoError(t, err)
	base64EncryptedKey := base64.StdEncoding.EncodeToString(encryptedKey)

	_, err = s.ds.SetOrUpdateHostDiskEncryptionKey(ctx, host, base64EncryptedKey, "", nil)
	require.NoError(t, err)

	// get that host - it has an encryption key with unknown decryptability, so
	// it should report "verifying" disk encryption.
	getHostResp = getHostResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &getHostResp)
	require.NotNil(t, getHostResp.Host.MDM.MacOSSettings.DiskEncryption)
	require.Equal(t, fleet.DiskEncryptionVerifying, *getHostResp.Host.MDM.MacOSSettings.DiskEncryption)
	require.Nil(t, getHostResp.Host.MDM.MacOSSettings.ActionRequired)
	require.NotNil(t, getHostResp.Host.MDM.OSSettings)
	require.NotNil(t, getHostResp.Host.MDM.OSSettings.DiskEncryption.Status)
	require.Equal(t, fleet.DiskEncryptionVerifying, *getHostResp.Host.MDM.OSSettings.DiskEncryption.Status)
	require.Equal(t, "", getHostResp.Host.MDM.OSSettings.DiskEncryption.Detail)

	// request with no token
	res := s.DoRawNoAuth("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/encryption_key", host.ID), nil, http.StatusUnauthorized)
	res.Body.Close()

	// decryption status not processed yet, but we'll still return the key on request if we can decrypt it
	resp := getHostEncryptionKeyResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/encryption_key", host.ID), nil, http.StatusOK, &resp)
	require.Equal(t, recoveryKey, resp.EncryptionKey.DecryptedValue)

	// simulate the cron marking the decryption status as false
	err = s.ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{host.ID}, false, time.Now())
	require.NoError(t, err)

	// even if decryption status was set to false, we still try to decrypt when the key is requested
	resp = getHostEncryptionKeyResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/encryption_key", host.ID), nil, http.StatusOK, &resp)
	require.Equal(t, recoveryKey, resp.EncryptionKey.DecryptedValue)

	// get that host - it has an encryption key that is un-decryptable, so it
	// should report "action_required" disk encryption and "rotate_key" action.
	getHostResp = getHostResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &getHostResp)
	require.NotNil(t, getHostResp.Host.MDM.MacOSSettings.DiskEncryption)
	require.Equal(t, fleet.DiskEncryptionActionRequired, *getHostResp.Host.MDM.MacOSSettings.DiskEncryption)
	require.NotNil(t, getHostResp.Host.MDM.MacOSSettings.ActionRequired)
	require.Equal(t, fleet.ActionRequiredRotateKey, *getHostResp.Host.MDM.MacOSSettings.ActionRequired)
	require.NotNil(t, getHostResp.Host.MDM.OSSettings)
	require.NotNil(t, getHostResp.Host.MDM.OSSettings.DiskEncryption.Status)
	require.Equal(t, fleet.DiskEncryptionActionRequired, *getHostResp.Host.MDM.OSSettings.DiskEncryption.Status)
	require.Equal(t, "", getHostResp.Host.MDM.OSSettings.DiskEncryption.Detail)

	// no activities created so far
	activities := listActivitiesResponse{}
	s.DoJSON("GET", "/api/latest/fleet/activities", nil, http.StatusOK, &activities)
	count := 0
	for _, activity := range activities.Activities {
		if activity.Type == "read_host_disk_encryption_key" {
			count++
		}
	}
	require.Equal(t, 2, count) // two successful requests so far

	// decryptable key
	checkDecryptableKey := func(u fleet.User) {
		err = s.ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{host.ID}, true, time.Now())
		require.NoError(t, err)
		resp = getHostEncryptionKeyResponse{}
		s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/encryption_key", host.ID), nil, http.StatusOK, &resp)
		require.Equal(t, recoveryKey, resp.EncryptionKey.DecryptedValue)

		// use the admin token to get the activities
		currToken := s.token
		defer func() { s.token = currToken }()
		s.token = s.getTestAdminToken()
		s.lastActivityMatches(
			"read_host_disk_encryption_key",
			fmt.Sprintf(`{"host_display_name": "%s", "host_id": %d}`, host.DisplayName(), host.ID),
			0,
		)
	}

	team, err := s.ds.NewTeam(context.Background(), &fleet.Team{
		ID:          4827,
		Name:        "team1_" + t.Name(),
		Description: "desc team1_" + t.Name(),
	})
	require.NoError(t, err)

	// enable disk encryption on the team so the key is not deleted when the host is added
	teamSpecs := applyTeamSpecsRequest{Specs: []*fleet.TeamSpec{{
		Name: "team1_" + t.Name(),
		MDM: fleet.TeamSpecMDM{
			EnableDiskEncryption: optjson.SetBool(true),
		},
	}}}
	s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK)

	// we're about to mess up with the token, make sure to set it to the
	// default value when the test ends
	currToken := s.token
	t.Cleanup(func() { s.token = currToken })

	// admins are able to see the host encryption key
	s.token = s.getTestAdminToken()
	checkDecryptableKey(s.users["admin1@example.com"])

	// get that host - it has an encryption key that is decryptable, so it
	// should report "verified" disk encryption.
	getHostResp = getHostResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &getHostResp)
	require.NotNil(t, getHostResp.Host.MDM.MacOSSettings.DiskEncryption)
	require.Equal(t, fleet.DiskEncryptionVerified, *getHostResp.Host.MDM.MacOSSettings.DiskEncryption)
	require.Nil(t, getHostResp.Host.MDM.MacOSSettings.ActionRequired)
	require.NotNil(t, getHostResp.Host.MDM.OSSettings)
	require.NotNil(t, getHostResp.Host.MDM.OSSettings.DiskEncryption.Status)
	require.Equal(t, fleet.DiskEncryptionVerified, *getHostResp.Host.MDM.OSSettings.DiskEncryption.Status)
	require.Equal(t, "", getHostResp.Host.MDM.OSSettings.DiskEncryption.Detail)

	// maintainers are able to see the token
	u := s.users["user1@example.com"]
	s.token = s.getTestToken(u.Email, test.GoodPassword)
	checkDecryptableKey(u)

	// observers are able to see the token
	u = s.users["user2@example.com"]
	s.token = s.getTestToken(u.Email, test.GoodPassword)
	checkDecryptableKey(u)

	// add the host to a team
	err = s.ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&team.ID, []uint{host.ID}))
	require.NoError(t, err)

	// admins are still able to see the token
	s.token = s.getTestAdminToken()
	checkDecryptableKey(s.users["admin1@example.com"])

	// maintainers are still able to see the token
	u = s.users["user1@example.com"]
	s.token = s.getTestToken(u.Email, test.GoodPassword)
	checkDecryptableKey(u)

	// observers are still able to see the token
	u = s.users["user2@example.com"]
	s.token = s.getTestToken(u.Email, test.GoodPassword)
	checkDecryptableKey(u)

	// add a team member
	u = fleet.User{
		Name:       "test team user",
		Email:      "user1+team@example.com",
		GlobalRole: nil,
		Teams: []fleet.UserTeam{
			{
				Team: *team,
				Role: fleet.RoleMaintainer,
			},
		},
	}
	require.NoError(t, u.SetPassword(test.GoodPassword, 10, 10))
	_, err = s.ds.NewUser(ctx, &u)
	require.NoError(t, err)

	// members are able to see the token
	s.token = s.getTestToken(u.Email, test.GoodPassword)
	checkDecryptableKey(u)

	// create a separate team
	team2, err := s.ds.NewTeam(context.Background(), &fleet.Team{
		ID:          4828,
		Name:        "team2_" + t.Name(),
		Description: "desc team2_" + t.Name(),
	})
	require.NoError(t, err)
	// add a team member
	u = fleet.User{
		Name:       "test team user",
		Email:      "user1+team2@example.com",
		GlobalRole: nil,
		Teams: []fleet.UserTeam{
			{
				Team: *team2,
				Role: fleet.RoleMaintainer,
			},
		},
	}
	require.NoError(t, u.SetPassword(test.GoodPassword, 10, 10))
	_, err = s.ds.NewUser(ctx, &u)
	require.NoError(t, err)

	// non-members aren't able to see the token
	s.token = s.getTestToken(u.Email, test.GoodPassword)
	resp = getHostEncryptionKeyResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/encryption_key", host.ID), nil, http.StatusForbidden, &resp)

	// clear the current key and archived key still exists
	_, err = s.ds.SetOrUpdateHostDiskEncryptionKey(ctx, host, "", "", nil)
	require.NoError(t, err)

	// switch to the admin token to get the key
	s.token = s.getTestAdminToken()
	resp = getHostEncryptionKeyResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/encryption_key", host.ID), nil, http.StatusOK, &resp)
	require.Equal(t, recoveryKey, resp.EncryptionKey.DecryptedValue) // old key is pulled from the archive

	detailsResp := getHostResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &detailsResp)
	require.False(t, detailsResp.Host.MDM.EncryptionKeyAvailable)
	require.NotNil(t, detailsResp.Host.MDM.EncryptionKeyArchived)
	require.True(t, *detailsResp.Host.MDM.EncryptionKeyArchived)
}

func (s *integrationMDMTestSuite) TestWindowsMDMGetEncryptionKey() {
	t := s.T()
	ctx := context.Background()

	// create a host and enroll it in Fleet
	host := createOrbitEnrolledHost(t, "windows", "h1", s.ds)
	err := s.ds.SetOrUpdateMDMData(ctx, host.ID, false, true, s.server.URL, false, fleet.WellKnownMDMFleet, "", false)
	require.NoError(t, err)

	// request encryption key with no auth token
	res := s.DoRawNoAuth("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/encryption_key", host.ID), nil, http.StatusUnauthorized)
	res.Body.Close()

	// no encryption key
	resp := getHostEncryptionKeyResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/encryption_key", host.ID), nil, http.StatusNotFound, &resp)

	// invalid host id
	resp = getHostEncryptionKeyResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/encryption_key", host.ID+999), nil, http.StatusNotFound, &resp)

	// add an encryption key for the host
	cert, _, _, err := s.fleetCfg.MDM.MicrosoftWSTEP()
	require.NoError(t, err)
	recoveryKey := "AAA-BBB-CCC"
	encryptedKey, err := microsoft_mdm.Encrypt(recoveryKey, cert.Leaf)
	require.NoError(t, err)

	_, err = s.ds.SetOrUpdateHostDiskEncryptionKey(ctx, host, encryptedKey, "", ptr.Bool(true))
	require.NoError(t, err)

	resp = getHostEncryptionKeyResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/encryption_key", host.ID), nil, http.StatusOK, &resp)
	require.Equal(t, host.ID, resp.HostID)
	require.Equal(t, recoveryKey, resp.EncryptionKey.DecryptedValue)
	s.lastActivityOfTypeMatches(fleet.ActivityTypeReadHostDiskEncryptionKey{}.ActivityName(),
		fmt.Sprintf(`{"host_display_name": "%s", "host_id": %d}`, host.DisplayName(), host.ID), 0)

	// update the key to blank with a client error
	_, err = s.ds.SetOrUpdateHostDiskEncryptionKey(ctx, host, "", "failed", nil)
	require.NoError(t, err)

	resp = getHostEncryptionKeyResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/encryption_key", host.ID), nil, http.StatusOK, &resp)
	require.Equal(t, recoveryKey, resp.EncryptionKey.DecryptedValue) // old key is pulled from the archive

	detailsResp := getHostResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &detailsResp)
	require.False(t, detailsResp.Host.MDM.EncryptionKeyAvailable)
	require.NotNil(t, detailsResp.Host.MDM.EncryptionKeyArchived)
	require.True(t, *detailsResp.Host.MDM.EncryptionKeyArchived)
}

func (s *integrationMDMTestSuite) TestAppConfigMDMAppleDiskEncryption() {
	t := s.T()

	// set the macos disk encryption field
	acResp := appConfigResponse{}
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
		"mdm": { "enable_disk_encryption": true }
  }`), http.StatusOK, &acResp)
	assert.True(t, acResp.MDM.EnableDiskEncryption.Value)
	enabledDiskActID := s.lastActivityMatches(fleet.ActivityTypeEnabledMacosDiskEncryption{}.ActivityName(),
		`{"team_id": null, "team_name": null}`, 0)

	// will have generated the macos config profile
	s.assertConfigProfilesByIdentifier(nil, mobileconfig.FleetFileVaultPayloadIdentifier, true)

	// check that they are returned by a GET /config
	acResp = appConfigResponse{}
	s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp)
	assert.True(t, acResp.MDM.EnableDiskEncryption.Value)

	// patch without specifying the macos disk encryption and an unrelated field,
	// should not alter it
	acResp = appConfigResponse{}
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
                     "mdm": { "macos_settings": {"custom_settings": [{"path": "a"}]} }
		}`), http.StatusOK, &acResp)
	assert.True(t, acResp.MDM.EnableDiskEncryption.Value)
	assert.Equal(t, []fleet.MDMProfileSpec{{Path: "a"}}, acResp.MDM.MacOSSettings.CustomSettings)
	s.lastActivityMatches(fleet.ActivityTypeEnabledMacosDiskEncryption{}.ActivityName(),
		``, enabledDiskActID)

	// patch with false, would reset it but this is a dry-run
	acResp = appConfigResponse{}
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
				"mdm": { "enable_disk_encryption": false }
		  }`), http.StatusOK, &acResp, "dry_run", "true")
	assert.True(t, acResp.MDM.EnableDiskEncryption.Value)
	assert.Equal(t, []fleet.MDMProfileSpec{{Path: "a"}}, acResp.MDM.MacOSSettings.CustomSettings)
	s.lastActivityMatches(fleet.ActivityTypeEnabledMacosDiskEncryption{}.ActivityName(),
		``, enabledDiskActID)

	// patch with false, resets it
	acResp = appConfigResponse{}
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
	    "mdm": { "enable_disk_encryption": false, "macos_settings": { "custom_settings": [{"path":"b"}] } }
		  }`), http.StatusOK, &acResp)
	assert.False(t, acResp.MDM.EnableDiskEncryption.Value)
	assert.Equal(t, []fleet.MDMProfileSpec{{Path: "b"}}, acResp.MDM.MacOSSettings.CustomSettings)
	s.lastActivityMatches(fleet.ActivityTypeDisabledMacosDiskEncryption{}.ActivityName(),
		`{"team_id": null, "team_name": null}`, 0)

	// will have deleted the macos config profile
	s.assertConfigProfilesByIdentifier(nil, mobileconfig.FleetFileVaultPayloadIdentifier, false)

	// use the MDM disk encryption endpoint to set it to true
	s.Do("POST", "/api/latest/fleet/disk_encryption",
		updateDiskEncryptionRequest{EnableDiskEncryption: true}, http.StatusNoContent)
	enabledDiskActID = s.lastActivityMatches(fleet.ActivityTypeEnabledMacosDiskEncryption{}.ActivityName(),
		`{"team_id": null, "team_name": null}`, 0)

	// will have created the macos config profile
	s.assertConfigProfilesByIdentifier(nil, mobileconfig.FleetFileVaultPayloadIdentifier, true)

	acResp = appConfigResponse{}
	s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp)
	assert.True(t, acResp.MDM.EnableDiskEncryption.Value)
	assert.Equal(t, []fleet.MDMProfileSpec{{Path: "b"}}, acResp.MDM.MacOSSettings.CustomSettings)

	// call update endpoint with no changes
	s.Do("PATCH", "/api/latest/fleet/mdm/apple/settings",
		fleet.MDMAppleSettingsPayload{}, http.StatusNoContent)
	s.lastActivityMatches(fleet.ActivityTypeEnabledMacosDiskEncryption{}.ActivityName(),
		``, enabledDiskActID)

	// the macos config profile still exists
	s.assertConfigProfilesByIdentifier(nil, mobileconfig.FleetFileVaultPayloadIdentifier, true)

	acResp = appConfigResponse{}
	s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp)
	assert.True(t, acResp.MDM.EnableDiskEncryption.Value)
	assert.Equal(t, []fleet.MDMProfileSpec{{Path: "b"}}, acResp.MDM.MacOSSettings.CustomSettings)

	// mdm/apple/settings works for windows as well as it's being used by
	// clients (UI) this way
	appConf, err := s.ds.AppConfig(context.Background())
	require.NoError(s.T(), err)
	appConf.MDM.EnabledAndConfigured = false
	appConf.MDM.WindowsEnabledAndConfigured = true
	err = s.ds.SaveAppConfig(context.Background(), appConf)
	require.NoError(s.T(), err)
	defer func() {
		appConf, err := s.ds.AppConfig(context.Background())
		require.NoError(s.T(), err)
		appConf.MDM.EnabledAndConfigured = true
		appConf.MDM.WindowsEnabledAndConfigured = true
		err = s.ds.SaveAppConfig(context.Background(), appConf)
		require.NoError(s.T(), err)
	}()

	// flip and verify the value
	s.Do("POST", "/api/latest/fleet/disk_encryption",
		updateDiskEncryptionRequest{EnableDiskEncryption: false}, http.StatusNoContent)
	acResp = appConfigResponse{}
	s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp)
	assert.False(t, acResp.MDM.EnableDiskEncryption.Value)

	s.Do("POST", "/api/latest/fleet/disk_encryption",
		updateDiskEncryptionRequest{EnableDiskEncryption: true}, http.StatusNoContent)
	acResp = appConfigResponse{}
	s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp)
	assert.True(t, acResp.MDM.EnableDiskEncryption.Value)
}

func (s *integrationMDMTestSuite) TestMDMAppleDiskEncryptionAggregate() {
	t := s.T()
	ctx := context.Background()

	// no hosts with any disk encryption status's
	expectedNoTeamDiskEncryptionSummary := fleet.MDMDiskEncryptionSummary{}
	s.checkMDMDiskEncryptionSummaries(t, nil, expectedNoTeamDiskEncryptionSummary, true)
	expectedNoTeamProfilesSummary := fleet.MDMProfilesSummary{}
	s.checkMDMProfilesSummaries(t, nil, expectedNoTeamProfilesSummary, &expectedNoTeamProfilesSummary)

	// 10 new hosts
	var hosts []*fleet.Host
	for i := 0; i < 10; i++ {
		h, err := s.ds.NewHost(ctx, &fleet.Host{
			DetailUpdatedAt: time.Now(),
			LabelUpdatedAt:  time.Now(),
			PolicyUpdatedAt: time.Now(),
			SeenTime:        time.Now().Add(-1 * time.Minute),
			OsqueryHostID:   ptr.String(fmt.Sprintf("%s-%d", t.Name(), i)),
			NodeKey:         ptr.String(fmt.Sprintf("%s-%d", t.Name(), i)),
			UUID:            fmt.Sprintf("%d-%s", i, uuid.New().String()),
			Hostname:        fmt.Sprintf("%sfoo.local", t.Name()),
			Platform:        "darwin",
		})
		require.NoError(t, err)
		hosts = append(hosts, h)
	}

	// no team tests ====

	// new filevault profile with no team
	prof, err := fleet.NewMDMAppleConfigProfile(mobileconfigForTest("filevault-1", mobileconfig.FleetFileVaultPayloadIdentifier), ptr.Uint(0))
	require.NoError(t, err)

	// generates a disk encryption aggregate value based on the arguments passed in
	generateAggregateValue := func(
		hosts []*fleet.Host,
		operationType fleet.MDMOperationType,
		status *fleet.MDMDeliveryStatus,
		decryptable bool,
	) {
		for _, host := range hosts {
			hostCmdUUID := uuid.New().String()
			err := s.ds.BulkUpsertMDMAppleHostProfiles(ctx, []*fleet.MDMAppleBulkUpsertHostProfilePayload{
				{
					ProfileUUID:       prof.ProfileUUID,
					ProfileIdentifier: prof.Identifier,
					HostUUID:          host.UUID,
					CommandUUID:       hostCmdUUID,
					OperationType:     operationType,
					Status:            status,
					Checksum:          []byte("csum"),
					Scope:             fleet.PayloadScopeSystem,
				},
			})
			require.NoError(t, err)
			oneMinuteAfterThreshold := time.Now().Add(+1 * time.Minute)
			_, err = s.ds.SetOrUpdateHostDiskEncryptionKey(ctx, host, "test-key", "", nil)
			require.NoError(t, err)
			err = s.ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{host.ID}, decryptable, oneMinuteAfterThreshold)
			require.NoError(t, err)
		}
	}

	// hosts 1,2 have disk encryption "applied" status
	generateAggregateValue(hosts[0:2], fleet.MDMOperationTypeInstall, &fleet.MDMDeliveryVerifying, true)
	expectedNoTeamDiskEncryptionSummary.Verifying.MacOS = 2
	s.checkMDMDiskEncryptionSummaries(t, nil, expectedNoTeamDiskEncryptionSummary, true)
	expectedNoTeamProfilesSummary.Verifying = 2
	s.checkMDMProfilesSummaries(t, nil, expectedNoTeamProfilesSummary, &expectedNoTeamProfilesSummary)

	// hosts 3,4 have disk encryption "action required" status
	generateAggregateValue(hosts[2:4], fleet.MDMOperationTypeInstall, &fleet.MDMDeliveryVerifying, false)
	expectedNoTeamDiskEncryptionSummary.ActionRequired.MacOS = 2
	s.checkMDMDiskEncryptionSummaries(t, nil, expectedNoTeamDiskEncryptionSummary, true)
	expectedNoTeamProfilesSummary.Pending = 2
	s.checkMDMProfilesSummaries(t, nil, expectedNoTeamProfilesSummary, &expectedNoTeamProfilesSummary)

	// hosts 5,6 have disk encryption "enforcing" status

	// host profiles status are `pending`
	generateAggregateValue(hosts[4:6], fleet.MDMOperationTypeInstall, &fleet.MDMDeliveryPending, true)
	expectedNoTeamDiskEncryptionSummary.Enforcing.MacOS = 2
	s.checkMDMDiskEncryptionSummaries(t, nil, expectedNoTeamDiskEncryptionSummary, true)
	expectedNoTeamProfilesSummary.Pending = 4
	s.checkMDMProfilesSummaries(t, nil, expectedNoTeamProfilesSummary, &expectedNoTeamProfilesSummary)

	// host profiles status dont exist
	generateAggregateValue(hosts[4:6], fleet.MDMOperationTypeInstall, nil, true)
	s.checkMDMDiskEncryptionSummaries(t, nil, expectedNoTeamDiskEncryptionSummary, true)               // no change
	s.checkMDMProfilesSummaries(t, nil, expectedNoTeamProfilesSummary, &expectedNoTeamProfilesSummary) // no change

	// host profile is applied but decryptable key does not exist
	mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
		_, err := q.ExecContext(
			context.Background(),
			"UPDATE host_disk_encryption_keys SET decryptable = NULL WHERE host_id IN (?, ?)",
			hosts[5].ID,
			hosts[6].ID,
		)
		require.NoError(t, err)
		return err
	})
	s.checkMDMDiskEncryptionSummaries(t, nil, expectedNoTeamDiskEncryptionSummary, true)               // no change
	s.checkMDMProfilesSummaries(t, nil, expectedNoTeamProfilesSummary, &expectedNoTeamProfilesSummary) // no change

	// hosts 7,8 have disk encryption "failed" status
	generateAggregateValue(hosts[6:8], fleet.MDMOperationTypeInstall, &fleet.MDMDeliveryFailed, true)
	expectedNoTeamDiskEncryptionSummary.Failed.MacOS = 2
	s.checkMDMDiskEncryptionSummaries(t, nil, expectedNoTeamDiskEncryptionSummary, true)
	expectedNoTeamProfilesSummary.Failed = 2
	s.checkMDMProfilesSummaries(t, nil, expectedNoTeamProfilesSummary, &expectedNoTeamProfilesSummary)

	// hosts 9,10 have disk encryption "removing enforcement" status
	generateAggregateValue(hosts[8:10], fleet.MDMOperationTypeRemove, &fleet.MDMDeliveryPending, true)
	expectedNoTeamDiskEncryptionSummary.RemovingEnforcement.MacOS = 2
	s.checkMDMDiskEncryptionSummaries(t, nil, expectedNoTeamDiskEncryptionSummary, true)
	expectedNoTeamProfilesSummary.Pending = 6
	s.checkMDMProfilesSummaries(t, nil, expectedNoTeamProfilesSummary, &expectedNoTeamProfilesSummary)

	// team tests ====

	// host 1,2 added to team 1
	tm, _ := s.ds.NewTeam(ctx, &fleet.Team{Name: "team-1"})
	err = s.ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&tm.ID, []uint{hosts[0].ID, hosts[1].ID}))
	require.NoError(t, err)

	// new filevault profile for team 1
	prof, err = fleet.NewMDMAppleConfigProfile(mobileconfigForTest("filevault-1", mobileconfig.FleetFileVaultPayloadIdentifier), ptr.Uint(1))
	require.NoError(t, err)
	prof.TeamID = &tm.ID
	require.NoError(t, err)

	// filtering by the "team_id" query param
	generateAggregateValue(hosts[0:2], fleet.MDMOperationTypeInstall, &fleet.MDMDeliveryVerifying, true)

	var expectedTeamDiskEncryptionSummary fleet.MDMDiskEncryptionSummary
	expectedTeamDiskEncryptionSummary.Verifying.MacOS = 2
	s.checkMDMDiskEncryptionSummaries(t, &tm.ID, expectedTeamDiskEncryptionSummary, true)

	expectedNoTeamDiskEncryptionSummary.Verifying.MacOS = 0 // now 0 because hosts 1,2 were added to team 1
	s.checkMDMDiskEncryptionSummaries(t, nil, expectedNoTeamDiskEncryptionSummary, true)

	expectedTeamProfilesSummary := fleet.MDMProfilesSummary{Verifying: 2}
	s.checkMDMProfilesSummaries(t, &tm.ID, expectedTeamProfilesSummary, &expectedTeamProfilesSummary)

	expectedNoTeamProfilesSummary = fleet.MDMProfilesSummary{
		Verifying: 0, // now 0 because hosts 1,2 were added to team 1
		Pending:   6,
		Failed:    2,
	}
	s.checkMDMProfilesSummaries(t, nil, expectedNoTeamProfilesSummary, &expectedNoTeamProfilesSummary)

	// verified status for host 1
	require.NoError(t, apple_mdm.VerifyHostMDMProfiles(ctx, s.ds, hosts[0], map[string]*fleet.HostMacOSProfile{prof.Identifier: {Identifier: prof.Identifier, DisplayName: prof.Name, InstallDate: time.Now()}}))
	// TODO: Why is there no change to the verification status of host 1 reflected in the summaries?
	s.checkMDMDiskEncryptionSummaries(t, &tm.ID, expectedTeamDiskEncryptionSummary, true)              // no change
	s.checkMDMDiskEncryptionSummaries(t, nil, expectedNoTeamDiskEncryptionSummary, true)               // no change
	s.checkMDMProfilesSummaries(t, &tm.ID, expectedTeamProfilesSummary, &expectedTeamProfilesSummary)  // no change
	s.checkMDMProfilesSummaries(t, nil, expectedNoTeamProfilesSummary, &expectedNoTeamProfilesSummary) // no change
}

func (s *integrationMDMTestSuite) TestTeamsMDMAppleDiskEncryption() {
	t := s.T()

	// create a team through the service so it initializes the agent ops
	teamName := t.Name() + "team1"
	team := &fleet.Team{
		Name:        teamName,
		Description: "desc team1",
	}
	var createTeamResp teamResponse
	s.DoJSON("POST", "/api/latest/fleet/teams", team, http.StatusOK, &createTeamResp)
	require.NotZero(t, createTeamResp.Team.ID)
	team = createTeamResp.Team

	// no macos config profile yet
	s.assertConfigProfilesByIdentifier(ptr.Uint(team.ID), mobileconfig.FleetFileVaultPayloadIdentifier, false)

	// apply with disk encryption
	teamSpecs := applyTeamSpecsRequest{Specs: []*fleet.TeamSpec{{
		Name: teamName,
		MDM: fleet.TeamSpecMDM{
			EnableDiskEncryption: optjson.SetBool(true),
		},
	}}}
	s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK)
	lastDiskActID := s.lastActivityOfTypeMatches(fleet.ActivityTypeEnabledMacosDiskEncryption{}.ActivityName(),
		fmt.Sprintf(`{"team_id": %d, "team_name": %q}`, team.ID, teamName), 0)

	// macos config profile created
	s.assertConfigProfilesByIdentifier(ptr.Uint(team.ID), mobileconfig.FleetFileVaultPayloadIdentifier, true)

	// retrieving the team returns the disk encryption setting
	var teamResp getTeamResponse
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), nil, http.StatusOK, &teamResp)
	require.True(t, teamResp.Team.Config.MDM.EnableDiskEncryption)

	// apply with invalid disk encryption value should fail
	teamSpecs = applyTeamSpecsRequest{Specs: []*fleet.TeamSpec{{
		Name: teamName,
		MDM: fleet.TeamSpecMDM{
			MacOSSettings: map[string]interface{}{"enable_disk_encryption": 123},
		},
	}}}
	res := s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusBadRequest)
	errMsg := extractServerErrorText(res.Body)
	assert.Contains(t, errMsg, `invalid value type at 'macos_settings.enable_disk_encryption': expected bool but got float64`)

	// apply an empty set of batch profiles to the team
	s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: nil},
		http.StatusUnprocessableEntity, "team_id", fmt.Sprint(team.ID), "team_name", team.Name)

	// the configuration profile is still there
	s.assertConfigProfilesByIdentifier(ptr.Uint(team.ID), mobileconfig.FleetFileVaultPayloadIdentifier, true)

	// apply without disk encryption settings specified and unrelated field,
	// should not replace existing disk encryption
	teamSpecs = applyTeamSpecsRequest{Specs: []*fleet.TeamSpec{{
		Name: teamName,
		MDM: fleet.TeamSpecMDM{
			MacOSSettings: map[string]interface{}{
				"custom_settings": []map[string]interface{}{
					{"path": "a"},
				},
			},
		},
	}}}
	s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK)
	teamResp = getTeamResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), nil, http.StatusOK, &teamResp)
	require.True(t, teamResp.Team.Config.MDM.EnableDiskEncryption)
	require.Equal(t, []fleet.MDMProfileSpec{{Path: "a"}}, teamResp.Team.Config.MDM.MacOSSettings.CustomSettings)
	s.lastActivityOfTypeMatches(fleet.ActivityTypeEnabledMacosDiskEncryption{}.ActivityName(),
		``, lastDiskActID)

	// apply with false would clear the existing setting, but dry-run
	teamSpecs = applyTeamSpecsRequest{Specs: []*fleet.TeamSpec{{
		Name: teamName,
		MDM: fleet.TeamSpecMDM{
			EnableDiskEncryption: optjson.SetBool(false),
		},
	}}}
	s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK, "dry_run", "true")
	teamResp = getTeamResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), nil, http.StatusOK, &teamResp)
	require.True(t, teamResp.Team.Config.MDM.EnableDiskEncryption)
	s.lastActivityOfTypeMatches(fleet.ActivityTypeEnabledMacosDiskEncryption{}.ActivityName(),
		``, lastDiskActID)

	// apply with false clears the existing setting
	teamSpecs = applyTeamSpecsRequest{Specs: []*fleet.TeamSpec{{
		Name: teamName,
		MDM: fleet.TeamSpecMDM{
			MacOSSettings: map[string]interface{}{"enable_disk_encryption": false},
		},
	}}}
	s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK)
	teamResp = getTeamResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), nil, http.StatusOK, &teamResp)
	require.False(t, teamResp.Team.Config.MDM.EnableDiskEncryption)
	s.lastActivityOfTypeMatches(fleet.ActivityTypeDisabledMacosDiskEncryption{}.ActivityName(),
		fmt.Sprintf(`{"team_id": %d, "team_name": %q}`, team.ID, teamName), 0)

	// macos config profile deleted
	s.assertConfigProfilesByIdentifier(ptr.Uint(team.ID), mobileconfig.FleetFileVaultPayloadIdentifier, false)

	// modify team's disk encryption via ModifyTeam endpoint
	var modResp teamResponse
	s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), fleet.TeamPayload{
		MDM: &fleet.TeamPayloadMDM{
			EnableDiskEncryption: optjson.SetBool(true),
		},
	}, http.StatusOK, &modResp)
	require.True(t, modResp.Team.Config.MDM.EnableDiskEncryption)
	s.lastActivityOfTypeMatches(fleet.ActivityTypeEnabledMacosDiskEncryption{}.ActivityName(),
		fmt.Sprintf(`{"team_id": %d, "team_name": %q}`, team.ID, teamName), 0)

	// macos config profile created
	s.assertConfigProfilesByIdentifier(ptr.Uint(team.ID), mobileconfig.FleetFileVaultPayloadIdentifier, true)

	// modify team's disk encryption and description via ModifyTeam endpoint
	modResp = teamResponse{}
	s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), fleet.TeamPayload{
		Description: ptr.String("foobar"),
		MDM: &fleet.TeamPayloadMDM{
			EnableDiskEncryption: optjson.SetBool(false),
		},
	}, http.StatusOK, &modResp)
	require.False(t, modResp.Team.Config.MDM.EnableDiskEncryption)
	require.Equal(t, "foobar", modResp.Team.Description)
	s.lastActivityOfTypeMatches(fleet.ActivityTypeDisabledMacosDiskEncryption{}.ActivityName(),
		fmt.Sprintf(`{"team_id": %d, "team_name": %q}`, team.ID, teamName), 0)

	// macos config profile deleted
	s.assertConfigProfilesByIdentifier(ptr.Uint(team.ID), mobileconfig.FleetFileVaultPayloadIdentifier, false)

	// use the MDM settings endpoint to set it to true
	s.Do("POST", "/api/latest/fleet/disk_encryption",
		updateDiskEncryptionRequest{TeamID: ptr.Uint(team.ID), EnableDiskEncryption: true}, http.StatusNoContent)
	lastDiskActID = s.lastActivityOfTypeMatches(fleet.ActivityTypeEnabledMacosDiskEncryption{}.ActivityName(),
		fmt.Sprintf(`{"team_id": %d, "team_name": %q}`, team.ID, teamName), 0)

	// macos config profile created
	s.assertConfigProfilesByIdentifier(ptr.Uint(team.ID), mobileconfig.FleetFileVaultPayloadIdentifier, true)

	teamResp = getTeamResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), nil, http.StatusOK, &teamResp)
	require.True(t, teamResp.Team.Config.MDM.EnableDiskEncryption)

	// use the MDM settings endpoint with no changes
	s.Do("PATCH", "/api/latest/fleet/mdm/apple/settings",
		fleet.MDMAppleSettingsPayload{TeamID: ptr.Uint(team.ID)}, http.StatusNoContent)
	s.lastActivityOfTypeMatches(fleet.ActivityTypeEnabledMacosDiskEncryption{}.ActivityName(),
		``, lastDiskActID)

	// macos config profile still exists
	s.assertConfigProfilesByIdentifier(ptr.Uint(team.ID), mobileconfig.FleetFileVaultPayloadIdentifier, true)

	teamResp = getTeamResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), nil, http.StatusOK, &teamResp)
	require.True(t, teamResp.Team.Config.MDM.EnableDiskEncryption)

	// use the MDM settings endpoint with an unknown team id
	s.Do("POST", "/api/latest/fleet/disk_encryption",
		updateDiskEncryptionRequest{TeamID: ptr.Uint(9999), EnableDiskEncryption: true}, http.StatusNotFound)

	// mdm/apple/settings works for windows as well as it's being used by
	// clients (UI) this way
	appConf, err := s.ds.AppConfig(context.Background())
	require.NoError(s.T(), err)
	appConf.MDM.EnabledAndConfigured = false
	appConf.MDM.WindowsEnabledAndConfigured = true
	err = s.ds.SaveAppConfig(context.Background(), appConf)
	require.NoError(s.T(), err)
	defer func() {
		appConf, err := s.ds.AppConfig(context.Background())
		require.NoError(s.T(), err)
		appConf.MDM.EnabledAndConfigured = true
		appConf.MDM.WindowsEnabledAndConfigured = true
		err = s.ds.SaveAppConfig(context.Background(), appConf)
		require.NoError(s.T(), err)
	}()

	// flip and verify the value
	s.Do("POST", "/api/latest/fleet/disk_encryption",
		updateDiskEncryptionRequest{TeamID: ptr.Uint(team.ID), EnableDiskEncryption: false}, http.StatusNoContent)
	teamResp = getTeamResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), nil, http.StatusOK, &teamResp)
	require.False(t, teamResp.Team.Config.MDM.EnableDiskEncryption)

	s.Do("POST", "/api/latest/fleet/disk_encryption",
		updateDiskEncryptionRequest{TeamID: ptr.Uint(team.ID), EnableDiskEncryption: true}, http.StatusNoContent)
	teamResp = getTeamResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), nil, http.StatusOK, &teamResp)
	require.True(t, teamResp.Team.Config.MDM.EnableDiskEncryption)
}

func (s *integrationMDMTestSuite) TestEnrollOrbitAfterDEPSync() {
	t := s.T()
	ctx := context.Background()

	// create a host with minimal information and the serial, no uuid/osquery id
	// (as when created via DEP sync).
	dbZeroTime := time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)
	h, err := s.ds.NewHost(ctx, &fleet.Host{
		HardwareSerial:   uuid.New().String(),
		Platform:         "darwin",
		LastEnrolledAt:   dbZeroTime,
		DetailUpdatedAt:  dbZeroTime,
		RefetchRequested: true,
	})
	require.NoError(t, err)
	require.Equal(t, dbZeroTime, h.LastEnrolledAt)

	// create an enroll secret
	secret := uuid.New().String()
	var applyResp applyEnrollSecretSpecResponse
	s.DoJSON("POST", "/api/latest/fleet/spec/enroll_secret", applyEnrollSecretSpecRequest{
		Spec: &fleet.EnrollSecretSpec{
			Secrets: []*fleet.EnrollSecret{{Secret: secret}},
		},
	}, http.StatusOK, &applyResp)

	// enroll the host from orbit, it should match the host above via the serial
	var resp EnrollOrbitResponse
	hostUUID := uuid.New().String()
	h.ComputerName = "My Mac"
	h.HardwareModel = "MacBook Pro"
	s.DoJSON("POST", "/api/fleet/orbit/enroll", contract.EnrollOrbitRequest{
		EnrollSecret:   secret,
		HardwareUUID:   hostUUID, // will not match any existing host
		HardwareSerial: h.HardwareSerial,
		ComputerName:   h.ComputerName,
		HardwareModel:  h.HardwareModel,
	}, http.StatusOK, &resp)
	require.NotEmpty(t, resp.OrbitNodeKey)

	// fetch the host, it will match the one created above
	// (NOTE: cannot check the returned OrbitNodeKey, this field is not part of the response)
	var hostResp getHostResponse
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", h.ID), nil, http.StatusOK, &hostResp)
	require.Equal(t, h.ID, hostResp.Host.ID)
	require.NotEqual(t, dbZeroTime, hostResp.Host.LastEnrolledAt)
	assert.Equal(t, h.ComputerName, hostResp.Host.ComputerName)
	assert.Equal(t, h.HardwareModel, hostResp.Host.HardwareModel)
	assert.Equal(t, h.HardwareSerial, hostResp.Host.HardwareSerial)
	assert.Equal(t, h.DisplayName(), hostResp.Host.DisplayName)

	got, err := s.ds.LoadHostByOrbitNodeKey(ctx, resp.OrbitNodeKey)
	require.NoError(t, err)
	require.Equal(t, h.ID, got.ID)

	s.lastActivityMatches(
		"fleet_enrolled",
		fmt.Sprintf(`{"host_id": %v, "host_display_name": "%s", "host_serial": "%s"}`, h.ID, h.DisplayName(), h.HardwareSerial),
		0,
	)

	// enroll the host from osquery, it should match the same host
	var osqueryResp contract.EnrollOsqueryAgentResponse
	osqueryID := uuid.New().String()
	s.DoJSON("POST", "/api/osquery/enroll", contract.EnrollOsqueryAgentRequest{
		EnrollSecret:   secret,
		HostIdentifier: osqueryID, // osquery host_identifier may not be the same as the host UUID, simulate that here
		HostDetails: map[string]map[string]string{
			"system_info": {
				"uuid":            hostUUID,
				"hardware_serial": h.HardwareSerial,
			},
		},
	}, http.StatusOK, &osqueryResp)
	require.NotEmpty(t, osqueryResp.NodeKey)

	// load the host by osquery node key, should match the initial host
	got, err = s.ds.LoadHostByNodeKey(ctx, osqueryResp.NodeKey)
	require.NoError(t, err)
	require.Equal(t, h.ID, got.ID)
}

func (s *integrationMDMTestSuite) TestFleetdConfiguration() {
	t := s.T()
	s.assertConfigProfilesByIdentifier(nil, mobileconfig.FleetdConfigPayloadIdentifier, false)

	var applyResp applyEnrollSecretSpecResponse
	s.DoJSON("POST", "/api/latest/fleet/spec/enroll_secret", applyEnrollSecretSpecRequest{
		Spec: &fleet.EnrollSecretSpec{
			Secrets: []*fleet.EnrollSecret{{Secret: t.Name()}},
		},
	}, http.StatusOK, &applyResp)

	// a new fleetd configuration profile for "no team" is created
	s.awaitTriggerProfileSchedule(t)
	s.assertConfigProfilesByIdentifier(nil, mobileconfig.FleetdConfigPayloadIdentifier, true)

	// create a new team
	tm, err := s.ds.NewTeam(context.Background(), &fleet.Team{
		Name:        t.Name(),
		Description: "desc",
	})
	require.NoError(t, err)
	s.assertConfigProfilesByIdentifier(&tm.ID, mobileconfig.FleetdConfigPayloadIdentifier, false)

	// upload an ABM token
	s.enableABM(t.Name())

	// set the default bm assignment to that team
	acResp := appConfigResponse{}
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(fmt.Sprintf(`{
		"mdm": {
			"apple_business_manager": [{
			  "organization_name": %q,
			  "macos_team": %q,
			  "ios_team": %q,
			  "ipados_team": %q
			}]
		}
	}`, t.Name(), tm.Name, tm.Name, tm.Name)), http.StatusOK, &acResp)
	t.Cleanup(func() {
		s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
			"mdm": {
				"apple_business_manager": []
			}
		}`), http.StatusOK, &acResp)
	})

	// the team doesn't have any enroll secrets yet, a profile is created using the global enroll secret
	s.awaitTriggerProfileSchedule(t)
	p := s.assertConfigProfilesByIdentifier(&tm.ID, mobileconfig.FleetdConfigPayloadIdentifier, true)
	require.Contains(t, string(p.Mobileconfig), t.Name())

	// create an enroll secret for the team
	teamSpecs := applyTeamSpecsRequest{Specs: []*fleet.TeamSpec{{
		Name:    tm.Name,
		Secrets: &[]fleet.EnrollSecret{{Secret: t.Name() + "team-secret"}},
	}}}
	s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK)

	// a new fleetd configuration profile for that team is created
	s.awaitTriggerProfileSchedule(t)
	p = s.assertConfigProfilesByIdentifier(&tm.ID, mobileconfig.FleetdConfigPayloadIdentifier, true)
	require.Contains(t, string(p.Mobileconfig), t.Name()+"team-secret")

	// the old configuration profile is kept
	s.assertConfigProfilesByIdentifier(nil, mobileconfig.FleetdConfigPayloadIdentifier, true)
}

func (s *integrationMDMTestSuite) TestEnqueueMDMCommand() {
	ctx := context.Background()
	t := s.T()
	s.setSkipWorkerJobs(t)

	// list commands should return all the commands we sent
	var listCmdResp0 listMDMAppleCommandsResponse
	s.DoJSON("GET", "/api/latest/fleet/mdm/apple/commands", nil, http.StatusOK, &listCmdResp0)
	require.Empty(t, listCmdResp0.Results)

	// Create host enrolled via osquery, but not enrolled in MDM.
	unenrolledHost := createHostAndDeviceToken(t, s.ds, "unused")

	// Create device enrolled in MDM but not enrolled via osquery.
	mdmDevice := mdmtest.NewTestMDMClientAppleDirect(mdmtest.AppleEnrollInfo{
		SCEPChallenge: s.scepChallenge,
		SCEPURL:       s.server.URL + apple_mdm.SCEPPath,
		MDMURL:        s.server.URL + apple_mdm.MDMPath,
	}, "MacBookPro16,1")
	err := mdmDevice.Enroll()
	require.NoError(t, err)

	base64Cmd := func(rawCmd string) string {
		return base64.RawStdEncoding.EncodeToString([]byte(rawCmd))
	}

	newRawCmd := func(cmdUUID string) string {
		return fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Command</key>
    <dict>
        <key>ManagedOnly</key>
        <false/>
        <key>RequestType</key>
        <string>ProfileList</string>
    </dict>
    <key>CommandUUID</key>
    <string>%s</string>
</dict>
</plist>`, cmdUUID)
	}

	// call with unknown host UUID
	uuid1 := uuid.New().String()
	s.Do("POST", "/api/latest/fleet/mdm/apple/enqueue",
		enqueueMDMAppleCommandRequest{
			// explicitly use standard encoding to make sure it also works
			// see #11384
			Command:   base64.StdEncoding.EncodeToString([]byte(newRawCmd(uuid1))),
			DeviceIDs: []string{"no-such-host"},
		}, http.StatusNotFound)

	// get command results returns 404, that command does not exist
	var cmdResResp getMDMAppleCommandResultsResponse
	s.DoJSON("GET", "/api/latest/fleet/mdm/apple/commandresults", nil, http.StatusNotFound, &cmdResResp, "command_uuid", uuid1)
	var getMDMCmdResp getMDMCommandResultsResponse
	s.DoJSON("GET", "/api/latest/fleet/commands/results", nil, http.StatusNotFound, &cmdResResp, "command_uuid", uuid1)

	// list commands returns empty set
	var listCmdResp listMDMAppleCommandsResponse
	s.DoJSON("GET", "/api/latest/fleet/mdm/apple/commands", nil, http.StatusOK, &listCmdResp)
	require.Empty(t, listCmdResp.Results)

	// call with unenrolled host UUID
	res := s.Do("POST", "/api/latest/fleet/mdm/apple/enqueue",
		enqueueMDMAppleCommandRequest{
			Command:   base64Cmd(newRawCmd(uuid.New().String())),
			DeviceIDs: []string{unenrolledHost.UUID},
		}, http.StatusBadRequest)
	errMsg := extractServerErrorText(res.Body)
	require.Contains(t, errMsg, "at least one of the hosts is not enrolled in MDM")

	// create a new Host to get the UUID on the DB
	linuxHost := createOrbitEnrolledHost(t, "linux", "h1", s.ds)
	windowsHost := createOrbitEnrolledHost(t, "windows", "h2", s.ds)
	// call with unenrolled host UUID
	res = s.Do("POST", "/api/latest/fleet/mdm/apple/enqueue",
		enqueueMDMAppleCommandRequest{
			Command:   base64Cmd(newRawCmd(uuid.New().String())),
			DeviceIDs: []string{linuxHost.UUID, windowsHost.UUID},
		}, http.StatusBadRequest)
	errMsg = extractServerErrorText(res.Body)
	require.Contains(t, errMsg, "at least one of the hosts is not enrolled in MDM or is not an elegible device")

	// call with payload that is not a valid, plist-encoded MDM command
	res = s.Do("POST", "/api/latest/fleet/mdm/apple/enqueue",
		enqueueMDMAppleCommandRequest{
			Command:   base64Cmd(string(mobileconfigForTest("test config profile", uuid.New().String()))),
			DeviceIDs: []string{mdmDevice.UUID},
		}, http.StatusUnsupportedMediaType)
	errMsg = extractServerErrorText(res.Body)
	require.Contains(t, errMsg, "unable to decode plist command")

	// call with enrolled host UUID
	uuid2 := uuid.New().String()
	rawCmd := newRawCmd(uuid2)
	var resp enqueueMDMAppleCommandResponse
	s.DoJSON("POST", "/api/latest/fleet/mdm/apple/enqueue",
		enqueueMDMAppleCommandRequest{
			Command:   base64Cmd(rawCmd),
			DeviceIDs: []string{mdmDevice.UUID},
		}, http.StatusOK, &resp)
	require.NotEmpty(t, resp.CommandUUID)
	require.Contains(t, rawCmd, resp.CommandUUID)
	require.Equal(t, resp.Platform, "darwin")
	require.Empty(t, resp.FailedUUIDs)
	require.Equal(t, "ProfileList", resp.RequestType)

	// the command exists but no results yet
	s.DoJSON("GET", "/api/latest/fleet/mdm/apple/commandresults", nil, http.StatusOK, &cmdResResp, "command_uuid", uuid2)
	require.Len(t, cmdResResp.Results, 0)
	s.DoJSON("GET", "/api/latest/fleet/commands/results", nil, http.StatusOK, &getMDMCmdResp, "command_uuid", uuid2)
	require.Len(t, getMDMCmdResp.Results, 0)

	// simulate a result and call again
	err = s.mdmStorage.StoreCommandReport(&mdm.Request{
		EnrollID: &mdm.EnrollID{ID: mdmDevice.UUID},
		Context:  ctx,
	}, &mdm.CommandResults{
		CommandUUID: uuid2,
		Status:      "Acknowledged",
		Raw:         []byte(rawCmd),
	})
	require.NoError(t, err)

	h, err := s.ds.HostByIdentifier(ctx, mdmDevice.UUID)
	require.NoError(t, err)
	h.Hostname = "test-host"
	err = s.ds.UpdateHost(ctx, h)
	require.NoError(t, err)

	s.DoJSON("GET", "/api/latest/fleet/mdm/apple/commandresults", nil, http.StatusOK, &cmdResResp, "command_uuid", uuid2)
	require.Len(t, cmdResResp.Results, 1)
	require.NotZero(t, cmdResResp.Results[0].UpdatedAt)
	cmdResResp.Results[0].UpdatedAt = time.Time{}
	require.Equal(t, &fleet.MDMCommandResult{
		HostUUID:    mdmDevice.UUID,
		CommandUUID: uuid2,
		Status:      "Acknowledged",
		RequestType: "ProfileList",
		Result:      []byte(rawCmd),
		Payload:     []byte(rawCmd),
		Hostname:    "test-host",
	}, cmdResResp.Results[0])

	s.DoJSON("GET", "/api/latest/fleet/commands/results", nil, http.StatusOK, &getMDMCmdResp, "command_uuid", uuid2)
	require.Len(t, getMDMCmdResp.Results, 1)
	require.NotZero(t, getMDMCmdResp.Results[0].UpdatedAt)
	getMDMCmdResp.Results[0].UpdatedAt = time.Time{}
	require.Equal(t, &fleet.MDMCommandResult{
		HostUUID:    mdmDevice.UUID,
		CommandUUID: uuid2,
		Status:      "Acknowledged",
		RequestType: "ProfileList",
		Result:      []byte(rawCmd),
		Payload:     []byte(rawCmd),
		Hostname:    "test-host",
	}, getMDMCmdResp.Results[0])

	// list commands returns that command
	s.DoJSON("GET", "/api/latest/fleet/mdm/apple/commands", nil, http.StatusOK, &listCmdResp)
	results, err := json.Marshal(listCmdResp.Results)
	require.NoError(t, err)
	t.Logf("GET /api/latest/fleet/mdm/apple/commands response:\n%s", results)

	// filter to the expected command.
	// there may be other commands due to the bootstrap packages uploaded in prior tests.
	// TODO: decouple these tests so we don't have to do this -- perhaps delete bootstrap packages?
	var profileListCommands []*fleet.MDMAppleCommand
	for _, result := range listCmdResp.Results {
		if result.RequestType == "ProfileList" {
			profileListCommands = append(profileListCommands, result)
		}
	}

	require.Len(t, profileListCommands, 1)
	require.NotZero(t, profileListCommands[0].UpdatedAt)
	profileListCommands[0].UpdatedAt = time.Time{}
	require.Equal(t, &fleet.MDMAppleCommand{
		DeviceID:    mdmDevice.UUID,
		CommandUUID: uuid2,
		Status:      "Acknowledged",
		RequestType: "ProfileList",
		Hostname:    "test-host",
	}, profileListCommands[0])
}

// setSkipWorkerJobs sets the skipWorkerJobs flag to true for the duration of the test.
// This avoids running into timing issues with the test.
// We can manually run the jobs if needed with s.runWorker().
func (s *integrationMDMTestSuite) setSkipWorkerJobs(t *testing.T) {
	s.skipWorkerJobs.Store(true)
	t.Cleanup(func() { s.skipWorkerJobs.Store(false) })
}

func (s *integrationMDMTestSuite) TestEnqueueMDMCommandWithSecret() {
	t := s.T()
	s.setSkipWorkerJobs(t)

	_, mdmClient := createHostThenEnrollMDM(s.ds, s.server.URL, t)

	// list commands should return all the commands we sent
	var listCmdResp listMDMAppleCommandsResponse
	s.DoJSON("GET", "/api/latest/fleet/mdm/apple/commands", nil, http.StatusOK, &listCmdResp, "host_identifier", mdmClient.UUID)
	require.Empty(t, listCmdResp.Results)

	base64Cmd := func(rawCmd string) string {
		return base64.RawStdEncoding.EncodeToString([]byte(rawCmd))
	}

	newRawCmd := func(cmdUUID string) string {
		return fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Command</key>
    <dict>
        <key>ManagedOnly</key>
        <false/>
        <key>RequestType</key>
        <string>ProfileList</string>
		<key>SecretValue</key>
		<string>$FLEET_SECRET_VALUE</string>
    </dict>
    <key>CommandUUID</key>
    <string>%s</string>
</dict>
</plist>`, cmdUUID)
	}

	// Load secret(s)
	secretValue := "*abc123*"
	req := createSecretVariablesRequest{
		SecretVariables: []fleet.SecretVariable{
			{
				Name:  "FLEET_SECRET_VALUE",
				Value: secretValue,
			},
		},
	}
	secretResp := createSecretVariablesResponse{}
	s.DoJSON("PUT", "/api/latest/fleet/spec/secret_variables", req, http.StatusOK, &secretResp)

	// call with enrolled host UUID
	uuid2 := uuid.New().String()
	rawCmd := newRawCmd(uuid2)
	var resp enqueueMDMAppleCommandResponse
	s.DoJSON("POST", "/api/latest/fleet/mdm/apple/enqueue",
		enqueueMDMAppleCommandRequest{
			Command:   base64Cmd(rawCmd),
			DeviceIDs: []string{mdmClient.UUID},
		}, http.StatusOK, &resp)
	require.NotEmpty(t, resp.CommandUUID)
	require.Contains(t, rawCmd, resp.CommandUUID)
	require.Equal(t, resp.Platform, "darwin")
	require.Empty(t, resp.FailedUUIDs)
	require.Equal(t, "ProfileList", resp.RequestType)

	// 1 command queued up
	s.DoJSON("GET", "/api/latest/fleet/mdm/apple/commands", nil, http.StatusOK, &listCmdResp, "host_identifier", mdmClient.UUID)
	require.Len(t, listCmdResp.Results, 1)

	cmd, err := mdmClient.Idle()
	require.NoError(t, err)
	assert.Contains(t, string(cmd.Raw), secretValue)
	assert.NotContains(t, string(cmd.Raw), "FLEET_SECRET_VALUE")
}

func (s *integrationMDMTestSuite) TestMDMWindowsCommandResults() {
	ctx := context.Background()
	t := s.T()

	h, err := s.ds.NewHost(ctx, &fleet.Host{
		Hostname:      "test-win-host-name",
		OsqueryHostID: ptr.String("1337"),
		NodeKey:       ptr.String("1337"),
		UUID:          "test-win-host-uuid",
		Platform:      "windows",
	})
	require.NoError(t, err)

	dev := &fleet.MDMWindowsEnrolledDevice{
		MDMDeviceID:            "test-device-id",
		MDMHardwareID:          "test-hardware-id",
		MDMDeviceState:         "ds",
		MDMDeviceType:          "dt",
		MDMDeviceName:          "dn",
		MDMEnrollType:          "et",
		MDMEnrollUserID:        "euid",
		MDMEnrollProtoVersion:  "epv",
		MDMEnrollClientVersion: "ecv",
		MDMNotInOOBE:           false,
		HostUUID:               h.UUID,
	}

	require.NoError(t, s.ds.MDMWindowsInsertEnrolledDevice(ctx, dev))
	var enrollmentID uint

	mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
		return sqlx.GetContext(ctx, q, &enrollmentID, `SELECT id FROM mdm_windows_enrollments WHERE mdm_device_id = ?`, dev.MDMDeviceID)
	})

	mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
		_, err := q.ExecContext(ctx,
			`UPDATE mdm_windows_enrollments SET host_uuid = ? WHERE id = ?`, dev.HostUUID, enrollmentID)
		return err
	})

	rawCmd := "some-command"
	cmdUUID := "some-uuid"
	cmdTarget := "some-target-loc-uri"

	mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
		_, err := q.ExecContext(ctx, `INSERT INTO windows_mdm_commands (command_uuid, raw_command, target_loc_uri) VALUES (?, ?, ?)`, cmdUUID, rawCmd, cmdTarget)
		return err
	})

	var responseID int64
	rawResponse := []byte("some-response")
	mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
		res, err := q.ExecContext(ctx, `INSERT INTO windows_mdm_responses (enrollment_id, raw_response) VALUES (?, ?)`, enrollmentID, rawResponse)
		if err != nil {
			return err
		}
		responseID, err = res.LastInsertId()
		return err
	})

	rawResult := []byte("some-result")
	statusCode := "200"
	mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
		_, err := q.ExecContext(ctx, `INSERT INTO windows_mdm_command_results (enrollment_id, command_uuid, raw_result, response_id, status_code) VALUES (?, ?, ?, ?, ?)`, enrollmentID, cmdUUID, rawResult, responseID, statusCode)
		return err
	})

	var resp getMDMCommandResultsResponse
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/commands/results?command_uuid=%s", cmdUUID), nil, http.StatusOK, &resp)
	require.Len(t, resp.Results, 1)
	require.Equal(t, dev.HostUUID, resp.Results[0].HostUUID)
	require.Equal(t, cmdUUID, resp.Results[0].CommandUUID)
	require.Equal(t, rawResponse, resp.Results[0].Result)
	require.Equal(t, cmdTarget, resp.Results[0].RequestType)
	require.Equal(t, statusCode, resp.Results[0].Status)
	require.Equal(t, h.Hostname, resp.Results[0].Hostname)

	resp = getMDMCommandResultsResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/commands/results?command_uuid=%s", uuid.New().String()), nil, http.StatusNotFound, &resp)
	require.Empty(t, resp.Results)
}

func (s *integrationMDMTestSuite) TestAppConfigMDMMacOSMigration() {
	t := s.T()

	checkDefaultAppConfig := func() {
		var ac appConfigResponse
		s.DoJSON("GET", "/api/v1/fleet/config", nil, http.StatusOK, &ac)
		require.False(t, ac.MDM.MacOSMigration.Enable)
		require.Empty(t, ac.MDM.MacOSMigration.Mode)
		require.Empty(t, ac.MDM.MacOSMigration.WebhookURL)
	}
	checkDefaultAppConfig()

	var acResp appConfigResponse
	// missing webhook_url
	s.DoJSON("PATCH", "/api/v1/fleet/config", json.RawMessage(`{
		"mdm": { "macos_migration": { "enable": true, "mode": "voluntary", "webhook_url": "" } }
  	}`), http.StatusUnprocessableEntity, &acResp)
	checkDefaultAppConfig()

	// invalid url scheme for webhook_url
	s.DoJSON("PATCH", "/api/v1/fleet/config", json.RawMessage(`{
		"mdm": { "macos_migration": { "enable": true, "mode": "voluntary", "webhook_url": "ftp://example.com" } }
	}`), http.StatusUnprocessableEntity, &acResp)
	checkDefaultAppConfig()

	// invalid mode
	s.DoJSON("PATCH", "/api/v1/fleet/config", json.RawMessage(`{
		"mdm": { "macos_migration": { "enable": true, "mode": "foobar", "webhook_url": "https://example.com" } }
  	}`), http.StatusUnprocessableEntity, &acResp)
	checkDefaultAppConfig()

	// valid request
	s.DoJSON("PATCH", "/api/v1/fleet/config", json.RawMessage(`{
		"mdm": { "macos_migration": { "enable": true, "mode": "voluntary", "webhook_url": "https://example.com" } }
	}`), http.StatusOK, &acResp)

	// confirm new app config
	s.DoJSON("GET", "/api/v1/fleet/config", nil, http.StatusOK, &acResp)
	require.True(t, acResp.MDM.MacOSMigration.Enable)
	require.Equal(t, fleet.MacOSMigrationModeVoluntary, acResp.MDM.MacOSMigration.Mode)
	require.Equal(t, "https://example.com", acResp.MDM.MacOSMigration.WebhookURL)
}

func (s *integrationMDMTestSuite) TestBootstrapPackage() {
	t := s.T()

	read := func(name string) []byte {
		b, err := os.ReadFile(filepath.Join("testdata", "bootstrap-packages", name))
		require.NoError(t, err)
		return b
	}
	invalidPkg := read("invalid.tar.gz")
	unsignedPkg := read("unsigned.pkg")
	wrongTOCPkg := read("wrong-toc.pkg")
	signedPkg := read("signed.pkg")

	// dry run query param parsing test
	s.uploadBootstrapPackage(&fleet.MDMAppleBootstrapPackage{Bytes: signedPkg, Name: "pkg.pkg"}, http.StatusOK, "", true)
	// verify no bootstrap package was uploaded
	var metadataResp bootstrapPackageMetadataResponse
	s.DoJSON("GET", "/api/latest/fleet/bootstrap/0/metadata", nil, http.StatusNotFound, &metadataResp)

	// empty bootstrap package
	s.uploadBootstrapPackage(&fleet.MDMAppleBootstrapPackage{}, http.StatusBadRequest, "package multipart field is required", false)
	// no name
	s.uploadBootstrapPackage(&fleet.MDMAppleBootstrapPackage{Bytes: signedPkg}, http.StatusBadRequest, "package multipart field is required", false)
	// invalid
	s.uploadBootstrapPackage(&fleet.MDMAppleBootstrapPackage{Bytes: invalidPkg, Name: "invalid.tar.gz"}, http.StatusBadRequest, "invalid file type", false)
	// invalid names
	for _, char := range file.InvalidMacOSChars {
		s.uploadBootstrapPackage(
			&fleet.MDMAppleBootstrapPackage{
				Bytes: signedPkg,
				Name:  fmt.Sprintf("invalid_%c_name.pkg", char),
			}, http.StatusBadRequest, "", false)
	}
	// unsigned
	s.uploadBootstrapPackage(&fleet.MDMAppleBootstrapPackage{Bytes: unsignedPkg, Name: "pkg.pkg"}, http.StatusBadRequest, "file is not signed", false)
	// wrong TOC
	s.uploadBootstrapPackage(&fleet.MDMAppleBootstrapPackage{Bytes: wrongTOCPkg, Name: "pkg.pkg"}, http.StatusBadRequest, "invalid package", false)
	// not a Distribution package
	s.uploadBootstrapPackage(&fleet.MDMAppleBootstrapPackage{Bytes: read("not-distribution-signed.pkg"), Name: "pkg.pkg"}, http.StatusBadRequest,
		fleet.BootstrapPkgNotDistributionErrMsg, false)
	// successfully upload a package
	s.uploadBootstrapPackage(&fleet.MDMAppleBootstrapPackage{Bytes: signedPkg, Name: "pkg.pkg", TeamID: 0}, http.StatusOK, "", false)
	// check the activity log
	s.lastActivityMatches(
		fleet.ActivityTypeAddedBootstrapPackage{}.ActivityName(),
		`{"bootstrap_package_name": "pkg.pkg", "team_id": null, "team_name": null}`,
		0,
	)

	// get package metadata
	s.DoJSON("GET", "/api/latest/fleet/bootstrap/0/metadata", nil, http.StatusOK, &metadataResp)
	require.Equal(t, metadataResp.MDMAppleBootstrapPackage.Name, "pkg.pkg")
	require.NotEmpty(t, metadataResp.MDMAppleBootstrapPackage.Sha256, "")
	require.NotEmpty(t, metadataResp.MDMAppleBootstrapPackage.Token)

	// download a package, wrong token
	var downloadResp downloadBootstrapPackageResponse
	s.DoJSON("GET", "/api/latest/fleet/bootstrap?token=bad", nil, http.StatusNotFound, &downloadResp)

	resp := s.DoRaw("GET", fmt.Sprintf("/api/latest/fleet/bootstrap?token=%s", metadataResp.MDMAppleBootstrapPackage.Token), nil, http.StatusOK)
	respBytes, err := io.ReadAll(resp.Body)
	require.NoError(t, err)
	require.EqualValues(t, signedPkg, respBytes)

	// missing package
	metadataResp = bootstrapPackageMetadataResponse{}
	s.DoJSON("GET", "/api/latest/fleet/bootstrap/1/metadata", nil, http.StatusNotFound, &metadataResp)

	// delete package
	var deleteResp deleteBootstrapPackageResponse
	s.DoJSON("DELETE", "/api/latest/fleet/bootstrap/0", nil, http.StatusOK, &deleteResp)
	// check the activity log
	s.lastActivityMatches(
		fleet.ActivityTypeDeletedBootstrapPackage{}.ActivityName(),
		`{"bootstrap_package_name": "pkg.pkg", "team_id": null, "team_name": null}`,
		0,
	)

	metadataResp = bootstrapPackageMetadataResponse{}
	s.DoJSON("GET", "/api/latest/fleet/bootstrap/0/metadata", nil, http.StatusNotFound, &metadataResp)
	// trying to delete again is a bad request
	s.DoJSON("DELETE", "/api/latest/fleet/bootstrap/0", nil, http.StatusNotFound, &deleteResp)
}

func (s *integrationMDMTestSuite) TestBootstrapPackageStatus() {
	t := s.T()

	abmOrgName := "abm_org"
	s.enableABM(abmOrgName)

	pkg, err := os.ReadFile(filepath.Join("testdata", "bootstrap-packages", "signed.pkg"))
	require.NoError(t, err)

	// upload a bootstrap package for "no team"
	s.uploadBootstrapPackage(&fleet.MDMAppleBootstrapPackage{Bytes: pkg, Name: "pkg.pkg", TeamID: 0}, http.StatusOK, "", false)

	// get package metadata
	var metadataResp bootstrapPackageMetadataResponse
	s.DoJSON("GET", "/api/latest/fleet/bootstrap/0/metadata", nil, http.StatusOK, &metadataResp)
	globalBootstrapPackage := metadataResp.MDMAppleBootstrapPackage

	// create a team and upload a bootstrap package for that team.
	teamName := t.Name() + "team1"
	team := &fleet.Team{
		Name:        teamName,
		Description: "desc team1",
	}
	var createTeamResp teamResponse
	s.DoJSON("POST", "/api/latest/fleet/teams", team, http.StatusOK, &createTeamResp)
	require.NotZero(t, createTeamResp.Team.ID)
	team = createTeamResp.Team

	// upload a bootstrap package for the team
	s.uploadBootstrapPackage(&fleet.MDMAppleBootstrapPackage{Bytes: pkg, Name: "pkg.pkg", TeamID: team.ID}, http.StatusOK, "", false)

	// get package metadata
	metadataResp = bootstrapPackageMetadataResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/bootstrap/%d/metadata", team.ID), nil, http.StatusOK, &metadataResp)
	teamBootstrapPackage := metadataResp.MDMAppleBootstrapPackage

	type deviceWithResponse struct {
		bootstrapResponse string
		device            *mdmtest.TestAppleMDMClient
	}

	// Note: The responses specified here are not a 1:1 mapping of the possible responses specified
	// by Apple. Instead `enrollAndCheckBootstrapPackage` below uses them to simulate scenarios in
	// which a device may or may not send a response. For example, "Offline" means that no response
	// will be sent by the device, which should in turn be interpreted by Fleet as "Pending"). See
	// https://developer.apple.com/documentation/devicemanagement/installenterpriseapplicationresponse
	//
	// Below:
	// - Acknowledge means the device will enroll and acknowledge the request to install the bp
	// - Error means that the device will enroll and fail to install the bp
	// - Offline means that the device will enroll but won't acknowledge nor fail the bp request
	// - Pending means that the device won't enroll at all
	mdmEnrollInfo := mdmtest.AppleEnrollInfo{
		SCEPChallenge: s.scepChallenge,
		SCEPURL:       s.server.URL + apple_mdm.SCEPPath,
		MDMURL:        s.server.URL + apple_mdm.MDMPath,
	}
	noTeamDevices := []deviceWithResponse{
		{"Acknowledge", mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo, "MacBookPro16,1")},
		{"Acknowledge", mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo, "MacBookPro16,1")},
		{"Acknowledge", mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo, "MacBookPro16,1")},
		{"Error", mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo, "MacBookPro16,1")},
		{"Offline", mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo, "MacBookPro16,1")},
		{"Offline", mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo, "MacBookPro16,1")},
		{"Pending", mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo, "MacBookPro16,1")},
		{"Pending", mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo, "MacBookPro16,1")},
	}

	teamDevices := []deviceWithResponse{
		{"Acknowledge", mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo, "MacBookPro16,1")},
		{"Acknowledge", mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo, "MacBookPro16,1")},
		{"Error", mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo, "MacBookPro16,1")},
		{"Error", mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo, "MacBookPro16,1")},
		{"Error", mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo, "MacBookPro16,1")},
		{"Offline", mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo, "MacBookPro16,1")},
		{"Pending", mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo, "MacBookPro16,1")},
	}

	expectedSerialsByTeamAndStatus := make(map[uint]map[fleet.MDMBootstrapPackageStatus][]string)
	expectedSerialsByTeamAndStatus[0] = map[fleet.MDMBootstrapPackageStatus][]string{
		fleet.MDMBootstrapPackageInstalled: {noTeamDevices[0].device.SerialNumber, noTeamDevices[1].device.SerialNumber, noTeamDevices[2].device.SerialNumber},
		fleet.MDMBootstrapPackageFailed:    {noTeamDevices[3].device.SerialNumber},
		fleet.MDMBootstrapPackagePending:   {noTeamDevices[4].device.SerialNumber, noTeamDevices[5].device.SerialNumber, noTeamDevices[6].device.SerialNumber, noTeamDevices[7].device.SerialNumber},
	}
	expectedSerialsByTeamAndStatus[team.ID] = map[fleet.MDMBootstrapPackageStatus][]string{
		fleet.MDMBootstrapPackageInstalled: {teamDevices[0].device.SerialNumber, teamDevices[1].device.SerialNumber},
		fleet.MDMBootstrapPackageFailed:    {teamDevices[2].device.SerialNumber, teamDevices[3].device.SerialNumber, teamDevices[4].device.SerialNumber},
		fleet.MDMBootstrapPackagePending:   {teamDevices[5].device.SerialNumber, teamDevices[6].device.SerialNumber},
	}

	// for good measure, add a couple of manually enrolled hosts
	createHostThenEnrollMDM(s.ds, s.server.URL, t)
	createHostThenEnrollMDM(s.ds, s.server.URL, t)

	// create a non-macOS host
	winHost, err := s.ds.NewHost(context.Background(), &fleet.Host{
		OsqueryHostID: ptr.String("non-macos-host"),
		NodeKey:       ptr.String("non-macos-host"),
		UUID:          uuid.New().String(),
		Hostname:      fmt.Sprintf("%sfoo.local.non.macos", t.Name()),
		Platform:      "windows",
		MDM:           fleet.MDMHostData{},
	})
	require.NoError(t, err)

	err = s.ds.SetOrUpdateMDMData(context.Background(), winHost.ID, false, true, s.server.URL+apple_mdm.MDMPath, true, fleet.WellKnownMDMFleet, "", false)
	require.NoError(t, err)

	// create a host that's not enrolled into MDM
	_, err = s.ds.NewHost(context.Background(), &fleet.Host{
		OsqueryHostID: ptr.String("not-mdm-enrolled"),
		NodeKey:       ptr.String("not-mdm-enrolled"),
		UUID:          uuid.New().String(),
		Hostname:      fmt.Sprintf("%sfoo.local.not.enrolled", t.Name()),
		Platform:      "darwin",
	})
	require.NoError(t, err)

	mockRespDevices := noTeamDevices
	s.mockDEPResponse(abmOrgName, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
		encoder := json.NewEncoder(w)
		switch r.URL.Path {
		case "/session":
			err := encoder.Encode(map[string]string{"auth_session_token": "xyz"})
			require.NoError(t, err)
		case "/profile":
			err := encoder.Encode(godep.ProfileResponse{ProfileUUID: "abc"})
			require.NoError(t, err)
		case "/server/devices":
			err := encoder.Encode(godep.DeviceResponse{})
			require.NoError(t, err)
		case "/devices/sync":
			depResp := []godep.Device{}
			for _, gd := range mockRespDevices {
				depResp = append(depResp, godep.Device{SerialNumber: gd.device.SerialNumber})
			}
			err := encoder.Encode(godep.DeviceResponse{Devices: depResp})
			require.NoError(t, err)
		case "/profile/devices":
			_, _ = w.Write([]byte(`{}`))
		default:
			_, _ = w.Write([]byte(`{}`))
		}
	}))

	// trigger a dep sync
	s.runDEPSchedule()

	var summaryResp getMDMAppleBootstrapPackageSummaryResponse
	s.DoJSON("GET", "/api/latest/fleet/bootstrap/summary", nil, http.StatusOK, &summaryResp)
	require.Equal(t, fleet.MDMAppleBootstrapPackageSummary{Pending: uint(len(noTeamDevices))}, summaryResp.MDMAppleBootstrapPackageSummary)

	var lhr listHostsResponse
	s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &lhr, "team_id", "0", "bootstrap_package", "pending")
	require.Len(t, lhr.Hosts, len(noTeamDevices))

	// set the default bm assignment to `team`
	acResp := appConfigResponse{}
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(fmt.Sprintf(`{
		"mdm": {
			"apple_business_manager": [{
			  "organization_name": %q,
			  "macos_team": %q,
			  "ios_team": %q,
			  "ipados_team": %q
			}]
		}
	}`, abmOrgName, team.Name, team.Name, team.Name)), http.StatusOK, &acResp)
	t.Cleanup(func() {
		s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
			"mdm": {
				"apple_business_manager": []
			}
		}`), http.StatusOK, &acResp)
	})

	// trigger a dep sync
	mockRespDevices = teamDevices
	s.runDEPSchedule()

	summaryResp = getMDMAppleBootstrapPackageSummaryResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/bootstrap/summary?team_id=%d", team.ID), nil, http.StatusOK, &summaryResp)
	require.Equal(t, fleet.MDMAppleBootstrapPackageSummary{Pending: uint(len(teamDevices))}, summaryResp.MDMAppleBootstrapPackageSummary)

	mockErrorChain := []mdm.ErrorChain{
		{ErrorCode: 12021, ErrorDomain: "MCMDMErrorDomain", LocalizedDescription: "Unknown command", USEnglishDescription: "Unknown command"},
	}

	// devices send their responses
	enrollAndCheckBootstrapPackage := func(d *deviceWithResponse, bp *fleet.MDMAppleBootstrapPackage) {
		err := d.device.Enroll() // queues DEP post-enrollment worker job
		require.NoError(t, err)

		// process worker jobs
		s.runWorker()

		cmd, err := d.device.Idle()
		require.NoError(t, err)
		for cmd != nil {
			var fullCmd micromdm.CommandPayload
			require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd))

			// if the command is to install the bootstrap package
			if manifest := fullCmd.Command.InstallEnterpriseApplication.Manifest; manifest != nil {
				require.Equal(t, "InstallEnterpriseApplication", cmd.Command.RequestType)
				require.NotNil(t, manifest)
				require.Equal(t, "software-package", manifest.ManifestItems[0].Assets[0].Kind)
				wantURL, err := bp.URL(s.server.URL)
				require.NoError(t, err)
				require.Equal(t, wantURL, manifest.ManifestItems[0].Assets[0].URL)

				// respond to the command accordingly
				switch d.bootstrapResponse {
				case "Acknowledge":
					cmd, err = d.device.Acknowledge(cmd.CommandUUID)
					require.NoError(t, err)
					continue
				case "Error":
					cmd, err = d.device.Err(cmd.CommandUUID, mockErrorChain)
					require.NoError(t, err)
					continue
				case "Offline":
					// host is offline, can't process any more commands
					cmd = nil
					continue
				}
			}
			cmd, err = d.device.Acknowledge(cmd.CommandUUID)
			require.NoError(t, err)
		}
	}

	for _, d := range noTeamDevices {
		dd := d
		if dd.bootstrapResponse != "Pending" {
			enrollAndCheckBootstrapPackage(&dd, globalBootstrapPackage)
		}
	}

	for _, d := range teamDevices {
		dd := d
		if dd.bootstrapResponse != "Pending" {
			enrollAndCheckBootstrapPackage(&dd, teamBootstrapPackage)
		}
	}

	checkHostDetails := func(t *testing.T, hostID uint, hostUUID string, expectedStatus fleet.MDMBootstrapPackageStatus) {
		var hostResp getHostResponse
		s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", hostID), nil, http.StatusOK, &hostResp)
		require.NotNil(t, hostResp.Host)
		require.NotNil(t, hostResp.Host.MDM.MacOSSetup)
		require.Equal(t, hostResp.Host.MDM.MacOSSetup.BootstrapPackageName, "pkg.pkg")
		require.Equal(t, hostResp.Host.MDM.MacOSSetup.BootstrapPackageStatus, expectedStatus)
		if expectedStatus == fleet.MDMBootstrapPackageFailed {
			require.Equal(t, hostResp.Host.MDM.MacOSSetup.Detail, apple_mdm.FmtErrorChain(mockErrorChain))
		} else {
			require.Empty(t, hostResp.Host.MDM.MacOSSetup.Detail)
		}
		require.Nil(t, hostResp.Host.MDM.MacOSSetup.Result)

		var hostByIdentifierResp getHostResponse
		s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/identifier/%s", hostUUID), nil, http.StatusOK, &hostByIdentifierResp)
		require.NotNil(t, hostByIdentifierResp.Host)
		require.NotNil(t, hostByIdentifierResp.Host.MDM.MacOSSetup)
		require.Equal(t, hostByIdentifierResp.Host.MDM.MacOSSetup.BootstrapPackageStatus, expectedStatus)
		if expectedStatus == fleet.MDMBootstrapPackageFailed {
			require.Equal(t, hostResp.Host.MDM.MacOSSetup.Detail, apple_mdm.FmtErrorChain(mockErrorChain))
		} else {
			require.Empty(t, hostResp.Host.MDM.MacOSSetup.Detail)
		}
		require.Nil(t, hostResp.Host.MDM.MacOSSetup.Result)
	}

	checkHostAPIs := func(t *testing.T, status fleet.MDMBootstrapPackageStatus, teamID *uint) {
		var expectedSerials []string
		if teamID == nil {
			expectedSerials = expectedSerialsByTeamAndStatus[0][status]
		} else {
			expectedSerials = expectedSerialsByTeamAndStatus[*teamID][status]
		}

		listHostsPath := fmt.Sprintf("/api/latest/fleet/hosts?bootstrap_package=%s", status)
		if teamID != nil {
			listHostsPath += fmt.Sprintf("&team_id=%d", *teamID)
		}
		var listHostsResp listHostsResponse
		s.DoJSON("GET", listHostsPath, nil, http.StatusOK, &listHostsResp)
		require.NotNil(t, listHostsResp.Hosts)
		require.Len(t, listHostsResp.Hosts, len(expectedSerials))

		gotHostsBySerial := make(map[string]fleet.HostResponse)
		for _, h := range listHostsResp.Hosts {
			gotHostsBySerial[h.HardwareSerial] = h
		}
		require.Len(t, gotHostsBySerial, len(expectedSerials))

		for _, serial := range expectedSerials {
			require.Contains(t, gotHostsBySerial, serial)
			h := gotHostsBySerial[serial]

			// pending hosts don't have an UUID yet.
			if h.UUID != "" {
				checkHostDetails(t, h.ID, h.UUID, status)
			}
		}

		countPath := fmt.Sprintf("/api/latest/fleet/hosts/count?bootstrap_package=%s", status)
		if teamID != nil {
			countPath += fmt.Sprintf("&team_id=%d", *teamID)
		}
		var countResp countHostsResponse
		s.DoJSON("GET", countPath, nil, http.StatusOK, &countResp)
		require.Equal(t, countResp.Count, len(expectedSerials))
	}

	// check summary no team hosts
	summaryResp = getMDMAppleBootstrapPackageSummaryResponse{}
	s.DoJSON("GET", "/api/latest/fleet/bootstrap/summary", nil, http.StatusOK, &summaryResp)
	require.Equal(t, fleet.MDMAppleBootstrapPackageSummary{
		Installed: uint(3),
		Pending:   uint(4),
		Failed:    uint(1),
	}, summaryResp.MDMAppleBootstrapPackageSummary)

	checkHostAPIs(t, fleet.MDMBootstrapPackageInstalled, nil)
	checkHostAPIs(t, fleet.MDMBootstrapPackagePending, nil)
	checkHostAPIs(t, fleet.MDMBootstrapPackageFailed, nil)

	// check team summary
	summaryResp = getMDMAppleBootstrapPackageSummaryResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/bootstrap/summary?team_id=%d", team.ID), nil, http.StatusOK, &summaryResp)
	require.Equal(t, fleet.MDMAppleBootstrapPackageSummary{
		Installed: uint(2),
		Pending:   uint(2),
		Failed:    uint(3),
	}, summaryResp.MDMAppleBootstrapPackageSummary)

	checkHostAPIs(t, fleet.MDMBootstrapPackageInstalled, &team.ID)
	checkHostAPIs(t, fleet.MDMBootstrapPackagePending, &team.ID)
	checkHostAPIs(t, fleet.MDMBootstrapPackageFailed, &team.ID)
}

func (s *integrationMDMTestSuite) TestEULA() {
	t := s.T()
	pdfBytes := []byte("%PDF-1.pdf-contents")
	pdfName := "eula.pdf"
	pdfHash := sha256.Sum256(pdfBytes)

	// trying to get metadata about an EULA that hasn't been uploaded yet is an error
	metadataResp := getMDMEULAMetadataResponse{}
	s.DoJSON("GET", "/api/latest/fleet/setup_experience/eula/metadata", nil, http.StatusNotFound, &metadataResp)

	// trying to upload a file that is not a PDF fails
	s.uploadEULA(&fleet.MDMEULA{Bytes: []byte("should-fail"), Name: "should-fail.pdf"}, http.StatusBadRequest, "invalid file type")
	// trying to upload an empty file fails
	s.uploadEULA(&fleet.MDMEULA{Bytes: []byte{}, Name: "should-fail.pdf"}, http.StatusBadRequest, "invalid file type")

	// admin is able to upload a new EULA
	s.uploadEULA(&fleet.MDMEULA{Bytes: pdfBytes, Name: pdfName}, http.StatusOK, "")

	// get EULA metadata
	metadataResp = getMDMEULAMetadataResponse{}
	s.DoJSON("GET", "/api/latest/fleet/setup_experience/eula/metadata", nil, http.StatusOK, &metadataResp)
	require.NotEmpty(t, metadataResp.MDMEULA.Token)
	require.NotEmpty(t, metadataResp.MDMEULA.CreatedAt)
	require.Equal(t, pdfName, metadataResp.MDMEULA.Name)
	require.Equal(t, pdfHash[:], metadataResp.MDMEULA.Sha256)
	eulaToken := metadataResp.Token

	// download EULA
	resp := s.DoRaw("GET", fmt.Sprintf("/api/latest/fleet/setup_experience/eula/%s", eulaToken), nil, http.StatusOK)
	require.EqualValues(t, len(pdfBytes), resp.ContentLength)
	require.Equal(t, "application/pdf", resp.Header.Get("content-type"))
	respBytes, err := io.ReadAll(resp.Body)
	require.NoError(t, err)
	require.EqualValues(t, pdfBytes, respBytes)

	// try to download EULA with a bad token
	var downloadResp downloadBootstrapPackageResponse
	s.DoJSON("GET", "/api/latest/fleet/setup_experience/eula/bad-token", nil, http.StatusNotFound, &downloadResp)

	// trying to upload any EULA without deleting the previous one first results in an error
	s.uploadEULA(&fleet.MDMEULA{Bytes: pdfBytes, Name: "should-fail.pdf"}, http.StatusConflict, "")

	// delete EULA
	var deleteResp deleteMDMEULAResponse
	s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/setup_experience/eula/%s", eulaToken), nil, http.StatusOK, &deleteResp)
	metadataResp = getMDMEULAMetadataResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/setup_experience/eula/%s", eulaToken), nil, http.StatusNotFound, &metadataResp)
	// trying to delete again is a bad request
	s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/setup_experience/eula/%s", eulaToken), nil, http.StatusNotFound, &deleteResp)
}

func (s *integrationMDMTestSuite) TestMigrateMDMDeviceWebhook() {
	t := s.T()

	h := createHostAndDeviceToken(t, s.ds, "good-token")

	var webhookCalled bool
	webhookSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		webhookCalled = true
		w.WriteHeader(http.StatusOK)
		switch r.URL.Path {
		case "/test_mdm_migration":
			var payload fleet.MigrateMDMDeviceWebhookPayload
			b, err := io.ReadAll(r.Body)
			require.NoError(t, err)
			err = json.Unmarshal(b, &payload)
			require.NoError(t, err)

			require.Equal(t, h.ID, payload.Host.ID)
			require.Equal(t, h.UUID, payload.Host.UUID)
			require.Equal(t, h.HardwareSerial, payload.Host.HardwareSerial)

		default:
			t.Errorf("unexpected request: %s", r.URL.Path)
		}
	}))
	defer webhookSrv.Close()

	// patch app config with webhook url
	acResp := fleet.AppConfig{}
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(fmt.Sprintf(`{
		"mdm": {
			"macos_migration": {
				"enable": true,
				"mode": "voluntary",
				"webhook_url": "%s/test_mdm_migration"
			}
		}
	}`, webhookSrv.URL)), http.StatusOK, &acResp)
	require.True(t, acResp.MDM.MacOSMigration.Enable)

	// expect errors when host is not eligible for migration
	isServer, enrolled, installedFromDEP := true, true, true
	mdmName := "ExampleMDM"
	mdmURL := "https://mdm.example.com"

	// host is a server so migration is not allowed
	require.NoError(t, s.ds.SetOrUpdateMDMData(context.Background(), h.ID, isServer, enrolled, mdmURL, installedFromDEP, mdmName, "", false))
	s.Do("POST", fmt.Sprintf("/api/v1/fleet/device/%s/migrate_mdm", "good-token"), nil, http.StatusBadRequest)
	require.False(t, webhookCalled)

	// host is not enrolled to MDM so migration is not allowed
	require.NoError(t, s.ds.SetOrUpdateMDMData(context.Background(), h.ID, !isServer, !enrolled, mdmURL, installedFromDEP, mdmName, "", false))
	s.Do("POST", fmt.Sprintf("/api/v1/fleet/device/%s/migrate_mdm", "good-token"), nil, http.StatusBadRequest)
	require.False(t, webhookCalled)

	// host is already enrolled to Fleet MDM so migration is not allowed
	require.NoError(t, s.ds.SetOrUpdateMDMData(context.Background(), h.ID, !isServer, enrolled, mdmURL, installedFromDEP, fleet.WellKnownMDMFleet, "", false))
	s.Do("POST", fmt.Sprintf("/api/v1/fleet/device/%s/migrate_mdm", "good-token"), nil, http.StatusBadRequest)
	require.False(t, webhookCalled)

	// up to this point, the refetch critical queries timestamp has not been set
	// on the host.
	h, err := s.ds.Host(context.Background(), h.ID)
	require.NoError(t, err)
	require.Nil(t, h.RefetchCriticalQueriesUntil)

	// host is enrolled to a third-party MDM but hasn't been assigned in
	// ABM yet, so migration is not allowed
	require.NoError(t, s.ds.SetOrUpdateMDMData(context.Background(), h.ID, !isServer, enrolled, mdmURL, installedFromDEP, mdmName, "", false))
	s.Do("POST", fmt.Sprintf("/api/v1/fleet/device/%s/migrate_mdm", "good-token"), nil, http.StatusBadRequest)
	require.False(t, webhookCalled)

	s.enableABM(t.Name())

	// simulate that the device is assigned to Fleet in ABM
	s.mockDEPResponse(t.Name(), http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
		switch r.URL.Path {
		case "/session":
			_, _ = w.Write([]byte(`{"auth_session_token": "xyz"}`))
		case "/profile":
			encoder := json.NewEncoder(w)
			err := encoder.Encode(godep.ProfileResponse{ProfileUUID: "abc"})
			require.NoError(t, err)
		case "/server/devices", "/devices/sync":
			encoder := json.NewEncoder(w)
			err := encoder.Encode(godep.DeviceResponse{
				Devices: []godep.Device{
					{
						SerialNumber: h.HardwareSerial,
						Model:        "Mac Mini",
						OS:           "osx",
						OpType:       "added",
					},
				},
			})
			require.NoError(t, err)

		case "/profile/devices":
			b, err := io.ReadAll(r.Body)
			require.NoError(t, err)
			var prof profileAssignmentReq
			require.NoError(t, json.Unmarshal(b, &prof))
			var resp godep.ProfileResponse
			resp.ProfileUUID = prof.ProfileUUID
			resp.Devices = map[string]string{
				prof.Devices[0]: string(fleet.DEPAssignProfileResponseSuccess),
			}
			encoder := json.NewEncoder(w)
			err = encoder.Encode(resp)
			require.NoError(t, err)
		}
	}))
	s.runDEPSchedule()

	// hosts meets all requirements, webhook is run
	s.Do("POST", fmt.Sprintf("/api/v1/fleet/device/%s/migrate_mdm", "good-token"), nil, http.StatusNoContent)
	require.True(t, webhookCalled)
	webhookCalled = false

	// the refetch critical queries timestamp has been set in the future
	h, err = s.ds.Host(context.Background(), h.ID)
	require.NoError(t, err)
	require.NotNil(t, h.RefetchCriticalQueriesUntil)
	require.True(t, h.RefetchCriticalQueriesUntil.After(time.Now()))

	// calling again works but does not trigger the webhook, as it was called recently
	s.Do("POST", fmt.Sprintf("/api/v1/fleet/device/%s/migrate_mdm", "good-token"), nil, http.StatusNoContent)
	require.False(t, webhookCalled)

	// setting the refetch critical queries timestamp in the past triggers the webhook again
	h.RefetchCriticalQueriesUntil = ptr.Time(time.Now().Add(-1 * time.Minute))
	err = s.ds.UpdateHost(context.Background(), h)
	require.NoError(t, err)

	s.Do("POST", fmt.Sprintf("/api/v1/fleet/device/%s/migrate_mdm", "good-token"), nil, http.StatusNoContent)
	require.True(t, webhookCalled)
	webhookCalled = false

	// host is manually enrolled, which is allowed
	h.RefetchCriticalQueriesUntil = ptr.Time(time.Now().Add(-1 * time.Minute))
	err = s.ds.UpdateHost(context.Background(), h)
	require.NoError(t, err)

	require.NoError(t, s.ds.SetOrUpdateMDMData(context.Background(), h.ID, !isServer, enrolled, mdmURL, !installedFromDEP, mdmName, "", false))
	s.Do("POST", fmt.Sprintf("/api/v1/fleet/device/%s/migrate_mdm", "good-token"), nil, http.StatusNoContent)
	require.True(t, webhookCalled)
	webhookCalled = false

	// the refetch critical queries timestamp has been updated to the future
	h, err = s.ds.Host(context.Background(), h.ID)
	require.NoError(t, err)
	require.NotNil(t, h.RefetchCriticalQueriesUntil)
	require.True(t, h.RefetchCriticalQueriesUntil.After(time.Now()))

	require.NoError(t, s.ds.UpdateHostRefetchCriticalQueriesUntil(context.Background(), h.ID, nil))

	// bad token
	s.Do("POST", fmt.Sprintf("/api/v1/fleet/device/%s/migrate_mdm", "bad-token"), nil, http.StatusUnauthorized)
	require.False(t, webhookCalled)

	// disable macos migration
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
		"mdm": {
			"macos_migration": {
				"enable": false,
				"mode": "voluntary",
				"webhook_url": ""
		      }
		}
	}`), http.StatusOK, &acResp)
	require.False(t, acResp.MDM.MacOSMigration.Enable)

	// expect error if macos migration is not configured
	s.Do("POST", fmt.Sprintf("/api/v1/fleet/device/%s/migrate_mdm", "good-token"), nil, http.StatusBadRequest)
	require.False(t, webhookCalled)
}

func (s *integrationMDMTestSuite) TestMigrateMDMDeviceWebhookErrors() {
	t := s.T()

	h := createHostAndDeviceToken(t, s.ds, "good-token")

	var webhookCalled bool
	webhookSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		webhookCalled = true
		w.WriteHeader(http.StatusBadRequest)
	}))
	defer webhookSrv.Close()

	// patch app config with webhook url
	acResp := fleet.AppConfig{}
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(fmt.Sprintf(`{
		"mdm": {
			"macos_migration": {
				"enable": true,
				"mode": "voluntary",
				"webhook_url": "%s/test_mdm_migration"
			}
		}
	}`, webhookSrv.URL)), http.StatusOK, &acResp)
	require.True(t, acResp.MDM.MacOSMigration.Enable)

	isServer, enrolled, installedFromDEP := true, true, true
	mdmName := "ExampleMDM"
	mdmURL := "https://mdm.example.com"

	// host is enrolled to a third-party MDM but hasn't been assigned in
	// ABM yet, so migration is not allowed
	require.NoError(t, s.ds.SetOrUpdateMDMData(context.Background(), h.ID, !isServer, enrolled, mdmURL, installedFromDEP, mdmName, "", false))
	s.Do("POST", fmt.Sprintf("/api/v1/fleet/device/%s/migrate_mdm", "good-token"), nil, http.StatusBadRequest)
	require.False(t, webhookCalled)

	s.enableABM(t.Name())
	// simulate that the device is assigned to Fleet in ABM
	s.mockDEPResponse(t.Name(), http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
		switch r.URL.Path {
		case "/session":
			_, _ = w.Write([]byte(`{"auth_session_token": "xyz"}`))
		case "/profile":
			encoder := json.NewEncoder(w)
			err := encoder.Encode(godep.ProfileResponse{ProfileUUID: "abc"})
			require.NoError(t, err)
		case "/server/devices", "/devices/sync":
			encoder := json.NewEncoder(w)
			err := encoder.Encode(godep.DeviceResponse{
				Devices: []godep.Device{
					{
						SerialNumber: h.HardwareSerial,
						Model:        "Mac Mini",
						OS:           "osx",
						OpType:       "added",
					},
				},
			})
			require.NoError(t, err)
		case "/profile/devices":
			b, err := io.ReadAll(r.Body)
			require.NoError(t, err)
			var prof profileAssignmentReq
			require.NoError(t, json.Unmarshal(b, &prof))
			var resp godep.ProfileResponse
			resp.ProfileUUID = prof.ProfileUUID
			resp.Devices = map[string]string{
				prof.Devices[0]: string(fleet.DEPAssignProfileResponseSuccess),
			}
			encoder := json.NewEncoder(w)
			err = encoder.Encode(resp)
			require.NoError(t, err)
		}
	}))
	s.runDEPSchedule()

	// hosts meets all requirements, webhook is run but returns an error, server should respond with
	// the same status code
	require.False(t, webhookCalled)
	s.Do("POST", fmt.Sprintf("/api/v1/fleet/device/%s/migrate_mdm", "good-token"), nil, http.StatusBadRequest)
	require.True(t, webhookCalled)
}

func (s *integrationMDMTestSuite) TestMDMMacOSSetup() {
	t := s.T()

	s.mockDEPResponse(t.Name(), http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
		encoder := json.NewEncoder(w)
		switch r.URL.Path {
		case "/session":
			err := encoder.Encode(map[string]string{"auth_session_token": "xyz"})
			require.NoError(t, err)
		case "/profile":
			err := encoder.Encode(godep.ProfileResponse{ProfileUUID: "abc"})
			require.NoError(t, err)
		default:
			_, _ = w.Write([]byte(`{}`))
		}
	}))

	// setup test data
	var acResp appConfigResponse
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
		"mdm": {
			"end_user_authentication": {
				"entity_id": "https://localhost:8080",
				"idp_name": "SimpleSAML",
				"metadata_url": "http://localhost:9080/simplesaml/saml2/idp/metadata.php"
		      }
		}
	}`), http.StatusOK, &acResp)
	require.NotEmpty(t, acResp.MDM.EndUserAuthentication)

	tm, err := s.ds.NewTeam(context.Background(), &fleet.Team{Name: "team1"})
	require.NoError(t, err)

	endUserAuthCases := []struct {
		raw      string
		expected bool
	}{
		{
			raw:      `"mdm": {}`,
			expected: false,
		},
		{
			raw: `"mdm": {
				"macos_setup": {}
			}`,
			expected: false,
		},
		{
			raw: `"mdm": {
				"macos_setup": {
					"enable_end_user_authentication": true
				}
			}`,
			expected: true,
		},
		{
			raw: `"mdm": {
				"macos_setup": {
					"enable_end_user_authentication": false
				}
			}`,
			expected: false,
		},
	}

	writeTmpJSON := func(t *testing.T, v any) string {
		tmpFile, err := os.CreateTemp(t.TempDir(), "*.json")
		require.NoError(t, err)
		err = json.NewEncoder(tmpFile).Encode(v)
		require.NoError(t, err)
		return tmpFile.Name()
	}

	mustReadFile := func(t *testing.T, path string) string {
		b, err := os.ReadFile(path)
		require.NoError(t, err)
		return string(b)
	}

	asstOk := writeTmpJSON(t, map[string]any{"ok": true})
	asstURL := writeTmpJSON(t, map[string]any{"url": "https://example.com"})
	asstAwait := writeTmpJSON(t, map[string]any{"await_device_configured": true})
	asstsByName := map[string]string{
		asstOk:    mustReadFile(t, asstOk),
		asstURL:   mustReadFile(t, asstURL),
		asstAwait: mustReadFile(t, asstAwait),
	}

	enableReleaseDeviceCases := []struct {
		enableRelease     *bool
		setupAssistant    string
		expectedRelease   bool
		expectedAssistant string
		expectedStatus    int
	}{
		{
			enableRelease:     nil,
			setupAssistant:    "",
			expectedRelease:   false,
			expectedAssistant: "",
			expectedStatus:    http.StatusOK,
		},
		{
			enableRelease:     ptr.Bool(true),
			setupAssistant:    "",
			expectedRelease:   true,
			expectedAssistant: "",
			expectedStatus:    http.StatusOK,
		},
		{
			enableRelease:     ptr.Bool(false),
			setupAssistant:    "",
			expectedRelease:   false,
			expectedAssistant: "",
			expectedStatus:    http.StatusOK,
		},
		{
			enableRelease:     ptr.Bool(false),
			setupAssistant:    asstURL,
			expectedRelease:   false,
			expectedAssistant: "",
			expectedStatus:    http.StatusUnprocessableEntity,
		},
		{
			enableRelease:     ptr.Bool(true),
			setupAssistant:    asstAwait,
			expectedRelease:   false,
			expectedAssistant: "",
			expectedStatus:    http.StatusUnprocessableEntity,
		},
	}

	t.Run("UpdateAppConfig", func(t *testing.T) {
		acResp := appConfigResponse{}
		path := "/api/latest/fleet/config"
		fmtJSON := func(s string) json.RawMessage {
			return json.RawMessage(fmt.Sprintf(`{
				%s
			}`, s))
		}

		// get the initial appconfig; enable end user authentication and release
		// device default is false
		s.DoJSON("GET", path, nil, http.StatusOK, &acResp)
		require.False(t, acResp.MDM.MacOSSetup.EnableEndUserAuthentication)
		require.False(t, acResp.MDM.MacOSSetup.EnableReleaseDeviceManually.Value)

		for i, c := range endUserAuthCases {
			t.Run(strconv.Itoa(i), func(t *testing.T) {
				acResp = appConfigResponse{}
				s.DoJSON("PATCH", path, fmtJSON(c.raw), http.StatusOK, &acResp)
				require.Equal(t, c.expected, acResp.MDM.MacOSSetup.EnableEndUserAuthentication)

				acResp = appConfigResponse{}
				s.DoJSON("GET", path, nil, http.StatusOK, &acResp)
				require.Equal(t, c.expected, acResp.MDM.MacOSSetup.EnableEndUserAuthentication)
			})
		}

		for i, c := range enableReleaseDeviceCases {
			t.Run(strconv.Itoa(i), func(t *testing.T) {
				macSetup := map[string]any{}
				if c.enableRelease != nil {
					macSetup["enable_release_device_manually"] = *c.enableRelease
				}
				if c.setupAssistant != "" {
					macSetup["macos_setup_assistant"] = c.setupAssistant
				}

				uploadSucceeded := true
				if c.setupAssistant != "" {
					s.Do("POST", "/api/v1/fleet/enrollment_profiles/automatic", createMDMAppleSetupAssistantRequest{
						Name:              c.setupAssistant,
						EnrollmentProfile: json.RawMessage(asstsByName[c.setupAssistant]),
					}, c.expectedStatus)
					if c.expectedStatus >= 300 {
						uploadSucceeded = false
					}
				}

				if uploadSucceeded {
					acResp = appConfigResponse{}
					s.DoJSON("PATCH", path,
						json.RawMessage(jsonMustMarshal(t, map[string]any{"mdm": map[string]any{"macos_setup": macSetup}})),
						c.expectedStatus, &acResp)
					require.Equal(t, c.expectedRelease, acResp.MDM.MacOSSetup.EnableReleaseDeviceManually.Value)
					require.Equal(t, c.expectedAssistant, acResp.MDM.MacOSSetup.MacOSSetupAssistant.Value)
				}

				acResp = appConfigResponse{}
				s.DoJSON("GET", path, nil, http.StatusOK, &acResp)
				require.Equal(t, c.expectedRelease, acResp.MDM.MacOSSetup.EnableReleaseDeviceManually.Value)
				require.Equal(t, c.expectedAssistant, acResp.MDM.MacOSSetup.MacOSSetupAssistant.Value)
			})
		}
	})

	t.Run("UpdateTeamConfig", func(t *testing.T) {
		path := fmt.Sprintf("/api/latest/fleet/teams/%d", tm.ID)
		fmtJSON := `{
			"name": %q,
			%s
		}`

		// get the initial team config; enable end user authentication and release
		// device default is false
		teamResp := teamResponse{}
		s.DoJSON("GET", path, nil, http.StatusOK, &teamResp)
		require.False(t, teamResp.Team.Config.MDM.MacOSSetup.EnableEndUserAuthentication)
		require.False(t, teamResp.Team.Config.MDM.MacOSSetup.EnableReleaseDeviceManually.Value)

		for i, c := range endUserAuthCases {
			t.Run(strconv.Itoa(i), func(t *testing.T) {
				teamResp = teamResponse{}
				s.DoJSON("PATCH", path, json.RawMessage(fmt.Sprintf(fmtJSON, tm.Name, c.raw)), http.StatusOK, &teamResp)
				require.Equal(t, c.expected, teamResp.Team.Config.MDM.MacOSSetup.EnableEndUserAuthentication)

				teamResp = teamResponse{}
				s.DoJSON("GET", path, nil, http.StatusOK, &teamResp)
				require.Equal(t, c.expected, teamResp.Team.Config.MDM.MacOSSetup.EnableEndUserAuthentication)
			})
		}

		for i, c := range enableReleaseDeviceCases {
			expectedPatchStatus := c.expectedStatus
			if expectedPatchStatus == http.StatusOK {
				expectedPatchStatus = http.StatusNoContent
			}

			t.Run(strconv.Itoa(i), func(t *testing.T) {
				if c.setupAssistant != "" {
					s.Do("POST", "/api/v1/fleet/enrollment_profiles/automatic", createMDMAppleSetupAssistantRequest{
						TeamID:            &tm.ID,
						Name:              c.setupAssistant,
						EnrollmentProfile: json.RawMessage(asstsByName[c.setupAssistant]),
					}, c.expectedStatus)
					uploadSucceeded := c.expectedStatus < 300

					if uploadSucceeded {
						// use the apply team specs to set both the setup assistant and the
						// enable release at once
						macSetup := fleet.MacOSSetup{
							MacOSSetupAssistant: optjson.SetString(c.setupAssistant),
						}
						if c.enableRelease != nil {
							macSetup.EnableReleaseDeviceManually = optjson.SetBool(*c.enableRelease)
						}
						teamSpecs := applyTeamSpecsRequest{Specs: []*fleet.TeamSpec{{
							Name: tm.Name,
							MDM:  fleet.TeamSpecMDM{MacOSSetup: macSetup},
						}}}
						s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK)
					}
				} else {
					// no setup assistant, use the PATCH /setup_experience endpoint
					payload := map[string]any{
						"team_id": tm.ID,
					}
					if c.enableRelease != nil {
						payload["enable_release_device_manually"] = *c.enableRelease
					}
					s.Do("PATCH", "/api/latest/fleet/setup_experience", json.RawMessage(jsonMustMarshal(t, payload)), expectedPatchStatus)
				}

				teamResp = teamResponse{}
				s.DoJSON("GET", path, nil, http.StatusOK, &teamResp)
				require.Equal(t, c.expectedRelease, teamResp.Team.Config.MDM.MacOSSetup.EnableReleaseDeviceManually.Value)
				require.Equal(t, c.expectedAssistant, teamResp.Team.Config.MDM.MacOSSetup.MacOSSetupAssistant.Value)
			})
		}
	})

	t.Run("TestMDMAppleSetupEndpoint", func(t *testing.T) {
		t.Run("TestNoTeam", func(t *testing.T) {
			var acResp appConfigResponse
			s.Do("PATCH", "/api/latest/fleet/setup_experience",
				fleet.MDMAppleSetupPayload{TeamID: ptr.Uint(0), EnableEndUserAuthentication: ptr.Bool(true)}, http.StatusNoContent)
			acResp = appConfigResponse{}
			s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp)
			require.True(t, acResp.MDM.MacOSSetup.EnableEndUserAuthentication)
			lastActivityID := s.lastActivityOfTypeMatches(fleet.ActivityTypeEnabledMacosSetupEndUserAuth{}.ActivityName(),
				`{"team_id": null, "team_name": null}`, 0)

			s.Do("PATCH", "/api/latest/fleet/setup_experience",
				fleet.MDMAppleSetupPayload{TeamID: ptr.Uint(0), EnableEndUserAuthentication: ptr.Bool(true)}, http.StatusNoContent)
			acResp = appConfigResponse{}
			s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp)
			require.True(t, acResp.MDM.MacOSSetup.EnableEndUserAuthentication)
			s.lastActivityOfTypeMatches(fleet.ActivityTypeEnabledMacosSetupEndUserAuth{}.ActivityName(),
				``, lastActivityID) // no new activity

			s.Do("PATCH", "/api/latest/fleet/setup_experience",
				fleet.MDMAppleSetupPayload{TeamID: ptr.Uint(0), EnableEndUserAuthentication: ptr.Bool(false)}, http.StatusNoContent)
			acResp = appConfigResponse{}
			s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp)
			require.False(t, acResp.MDM.MacOSSetup.EnableEndUserAuthentication)
			require.Greater(t, s.lastActivityOfTypeMatches(fleet.ActivityTypeDisabledMacosSetupEndUserAuth{}.ActivityName(),
				`{"team_id": null, "team_name": null}`, 0), lastActivityID)
		})

		t.Run("TestTeam", func(t *testing.T) {
			tmConfigPath := fmt.Sprintf("/api/latest/fleet/teams/%d", tm.ID)
			expectedActivityDetail := fmt.Sprintf(`{"team_id": %d, "team_name": %q}`, tm.ID, tm.Name)
			var tmResp teamResponse
			s.Do("PATCH", "/api/latest/fleet/setup_experience",
				fleet.MDMAppleSetupPayload{TeamID: &tm.ID, EnableEndUserAuthentication: ptr.Bool(true)}, http.StatusNoContent)
			tmResp = teamResponse{}
			s.DoJSON("GET", tmConfigPath, nil, http.StatusOK, &tmResp)
			require.True(t, tmResp.Team.Config.MDM.MacOSSetup.EnableEndUserAuthentication)
			lastActivityID := s.lastActivityOfTypeMatches(fleet.ActivityTypeEnabledMacosSetupEndUserAuth{}.ActivityName(),
				expectedActivityDetail, 0)

			s.Do("PATCH", "/api/latest/fleet/setup_experience",
				fleet.MDMAppleSetupPayload{TeamID: &tm.ID, EnableEndUserAuthentication: ptr.Bool(true)}, http.StatusNoContent)
			tmResp = teamResponse{}
			s.DoJSON("GET", tmConfigPath, nil, http.StatusOK, &tmResp)
			require.True(t, tmResp.Team.Config.MDM.MacOSSetup.EnableEndUserAuthentication)
			s.lastActivityOfTypeMatches(fleet.ActivityTypeEnabledMacosSetupEndUserAuth{}.ActivityName(),
				``, lastActivityID) // no new activity

			s.Do("PATCH", "/api/latest/fleet/setup_experience",
				fleet.MDMAppleSetupPayload{TeamID: &tm.ID, EnableEndUserAuthentication: ptr.Bool(false)}, http.StatusNoContent)
			tmResp = teamResponse{}
			s.DoJSON("GET", tmConfigPath, nil, http.StatusOK, &tmResp)
			require.False(t, tmResp.Team.Config.MDM.MacOSSetup.EnableEndUserAuthentication)
			require.Greater(t, s.lastActivityOfTypeMatches(fleet.ActivityTypeDisabledMacosSetupEndUserAuth{}.ActivityName(),
				expectedActivityDetail, 0), lastActivityID)
		})
	})

	t.Run("ValidateEnableEndUserAuthentication", func(t *testing.T) {
		// ensure the test is setup correctly
		var acResp appConfigResponse
		s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
			"mdm": {
				"end_user_authentication": {
					"entity_id": "https://localhost:8080",
					"idp_name": "SimpleSAML",
					"metadata_url": "http://localhost:9080/simplesaml/saml2/idp/metadata.php"
				},
				"macos_setup": {
					"enable_end_user_authentication": true
				}
			}
		}`), http.StatusOK, &acResp)
		require.NotEmpty(t, acResp.MDM.EndUserAuthentication)

		// ok to disable end user authentication without a configured IdP
		acResp = appConfigResponse{}
		s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
			"mdm": {
				"end_user_authentication": {
					"entity_id": "",
					"idp_name": "",
					"metadata_url": ""
				},
				"macos_setup": {
					"enable_end_user_authentication": false
				}
			}
		}`), http.StatusOK, &acResp)
		require.Equal(t, acResp.MDM.MacOSSetup.EnableEndUserAuthentication, false)
		require.True(t, acResp.MDM.EndUserAuthentication.IsEmpty())

		// can't enable end user authentication without a configured IdP
		s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
			"mdm": {
				"end_user_authentication": {
					"entity_id": "",
					"idp_name": "",
					"metadata_url": ""
				},
				"macos_setup": {
					"enable_end_user_authentication": true
				}
			}
		}`), http.StatusUnprocessableEntity, &acResp)

		// can't use setup endpoint to enable end user authentication on no team without a configured IdP
		s.Do("PATCH", "/api/latest/fleet/setup_experience",
			fleet.MDMAppleSetupPayload{TeamID: ptr.Uint(0), EnableEndUserAuthentication: ptr.Bool(true)}, http.StatusUnprocessableEntity)

		// can't enable end user authentication on team config without a configured IdP already on app config
		var teamResp teamResponse
		s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", tm.ID), json.RawMessage(fmt.Sprintf(`{
			"name": %q,
			"mdm": {
				"macos_setup": {
					"enable_end_user_authentication": true
				}
			}
		}`, tm.Name)), http.StatusUnprocessableEntity, &teamResp)

		// can't use setup endpoint to enable end user authentication on team without a configured IdP
		s.Do("PATCH", "/api/latest/fleet/setup_experience",
			fleet.MDMAppleSetupPayload{TeamID: &tm.ID, EnableEndUserAuthentication: ptr.Bool(true)}, http.StatusUnprocessableEntity)

		// ensure IdP is empty for the rest of the tests
		s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
			"mdm": {
				"end_user_authentication": {
					"entity_id": "",
					"idp_name": "",
					"metadata_url": ""
				}
			}
		}`), http.StatusOK, &acResp)
		require.Empty(t, acResp.MDM.EndUserAuthentication)
	})
}

func (s *integrationMDMTestSuite) TestMacosSetupAssistant() {
	ctx := context.Background()
	t := s.T()

	const defaultProf = `{
	"profile_name": "%s",
	"allow_pairing": true,
	"is_mdm_removable": true,
	"org_magic": "1",
	"language": "en",
	"region": "US",
	"skip_setup_items": [
		"Accessibility",
		"Appearance",
		"AppleID",
		"AppStore",
		"Biometric",
		"Diagnostics",
		"FileVault",
		"iCloudDiagnostics",
		"iCloudStorage",
		"Location",
		"Payment",
		"Privacy",
		"Restore",
		"ScreenTime",
		"Siri",
		"TermsOfAddress",
		"TOS",
		"UnlockWithWatch"
	]
}
`

	// Associate the token with the team
	s.enableABM(t.Name())
	// start a server that will mock the Apple DEP API
	s.mockDEPResponse(t.Name(), http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		encoder := json.NewEncoder(w)
		switch r.URL.Path {
		case "/session":
			_, _ = w.Write([]byte(`{"auth_session_token": "session123"}`))
		case "/account":
			_, _ = w.Write([]byte(fmt.Sprintf(`{"admin_id": "admin123", "org_name": "%s"}`, "foo")))
		case "/profile":
			body, err := io.ReadAll(r.Body)
			require.NoError(t, err)
			var prof godep.Profile
			require.NoError(t, json.Unmarshal(body, &prof))
			switch {
			case len(prof.ProfileName) > 125:
				w.WriteHeader(http.StatusBadRequest)
				require.NoError(t, encoder.Encode(map[string]any{"error": "CONFIG_NAME_INVALID"}))
			case prof.ProfileName == "":
				w.WriteHeader(http.StatusBadRequest)
				require.NoError(t, encoder.Encode(map[string]any{"error": "CONFIG_NAME_REQUIRED"}))
			case len(prof.ConfigurationWebURL) > 125:
				w.WriteHeader(http.StatusBadRequest)
				require.NoError(t, encoder.Encode(map[string]any{"error": "CONFIG_URL_INVALID"}))
			case len(prof.Department) > 125:
				w.WriteHeader(http.StatusBadRequest)
				require.NoError(t, encoder.Encode(map[string]any{"error": "DEPARTMENT_INVALID"}))
			case len(prof.SupportPhoneNumber) > 50:
				w.WriteHeader(http.StatusBadRequest)
				require.NoError(t, encoder.Encode(map[string]any{"error": "SUPPORT_PHONE_INVALID"}))
			case len(prof.SupportEmailAddress) > 250:
				w.WriteHeader(http.StatusBadRequest)
				require.NoError(t, encoder.Encode(map[string]any{"error": "SUPPORT_EMAIL_INVALID"}))
			case len(prof.OrgMagic) > 256:
				w.WriteHeader(http.StatusBadRequest)
				require.NoError(t, encoder.Encode(map[string]any{"error": "MAGIC_INVALID"}))
			case !prof.IsMDMRemovable && !prof.IsSupervised:
				w.WriteHeader(http.StatusBadRequest)
				require.NoError(t, encoder.Encode(map[string]any{"error": "FLAGS_INVALID"}))
			default:
				w.WriteHeader(http.StatusOK)
				require.NoError(t, encoder.Encode(godep.ProfileResponse{ProfileUUID: "profile123"}))
			}
		}
	}))

	// get for no team returns 404
	var getResp getMDMAppleSetupAssistantResponse
	s.DoJSON("GET", "/api/latest/fleet/enrollment_profiles/automatic", nil, http.StatusNotFound, &getResp)
	// get for non-existing team returns 404
	s.DoJSON("GET", "/api/latest/fleet/enrollment_profiles/automatic", nil, http.StatusNotFound, &getResp, "team_id", "123")

	// Profile name too long
	var createResp createMDMAppleSetupAssistantResponse
	r := s.Do("POST", "/api/latest/fleet/enrollment_profiles/automatic", createMDMAppleSetupAssistantRequest{
		TeamID:            nil,
		Name:              "profile_name_too_long",
		EnrollmentProfile: json.RawMessage(fmt.Sprintf(`{"profile_name": "%s"}`, strings.Repeat("a", 126))),
	}, http.StatusUnprocessableEntity)
	require.Contains(t, extractServerErrorText(r.Body), "CONFIG_NAME_INVALID")

	// Profile name missing
	r = s.Do("POST", "/api/latest/fleet/enrollment_profiles/automatic", createMDMAppleSetupAssistantRequest{
		TeamID:            nil,
		Name:              "profile_name_missing",
		EnrollmentProfile: json.RawMessage(`{"profile_name": ""}`),
	}, http.StatusUnprocessableEntity)
	require.Contains(t, extractServerErrorText(r.Body), "CONFIG_NAME_REQUIRED")

	// Config URL invalid
	r = s.Do("POST", "/api/latest/fleet/enrollment_profiles/automatic", createMDMAppleSetupAssistantRequest{
		TeamID:            nil,
		Name:              "profile_name_missing",
		EnrollmentProfile: json.RawMessage(fmt.Sprintf(`{"profile_name": "prof_name", "configuration_web_url": "%s"}`, strings.Repeat("a", 126))),
	}, http.StatusUnprocessableEntity)
	require.Contains(t, extractServerErrorText(r.Body), "CONFIG_URL_INVALID")

	// Department invalid
	r = s.Do("POST", "/api/latest/fleet/enrollment_profiles/automatic", createMDMAppleSetupAssistantRequest{
		TeamID:            nil,
		Name:              "profile_name_missing",
		EnrollmentProfile: json.RawMessage(fmt.Sprintf(`{"profile_name": "prof_name", "configuration_web_url": "https://example.com", "department": "%s"}`, strings.Repeat("a", 126))),
	}, http.StatusUnprocessableEntity)
	require.Contains(t, extractServerErrorText(r.Body), "DEPARTMENT_INVALID")

	// Invalid support phone
	r = s.Do("POST", "/api/latest/fleet/enrollment_profiles/automatic", createMDMAppleSetupAssistantRequest{
		TeamID:            nil,
		Name:              "profile_name_missing",
		EnrollmentProfile: json.RawMessage(fmt.Sprintf(`{"profile_name": "prof_name", "configuration_web_url": "https://example.com", "department": "foo", "support_phone_number": "%s"}`, strings.Repeat("1", 51))),
	}, http.StatusUnprocessableEntity)
	require.Contains(t, extractServerErrorText(r.Body), "SUPPORT_PHONE_INVALID")

	// Invalid support email
	r = s.Do("POST", "/api/latest/fleet/enrollment_profiles/automatic", createMDMAppleSetupAssistantRequest{
		TeamID:            nil,
		Name:              "profile_name_missing",
		EnrollmentProfile: json.RawMessage(fmt.Sprintf(`{"profile_name": "prof_name", "configuration_web_url": "https://example.com", "department": "foo", "support_phone_number": "555-123-4567", "support_email_address": "%s"}`, strings.Repeat("1", 251))),
	}, http.StatusUnprocessableEntity)
	require.Contains(t, extractServerErrorText(r.Body), "SUPPORT_EMAIL_INVALID")

	// Invalid magic
	r = s.Do("POST", "/api/latest/fleet/enrollment_profiles/automatic", createMDMAppleSetupAssistantRequest{
		TeamID:            nil,
		Name:              "profile_name_missing",
		EnrollmentProfile: json.RawMessage(fmt.Sprintf(`{"profile_name": "prof_name", "configuration_web_url": "https://example.com", "department": "foo", "support_phone_number": "555-123-4567", "support_email_address": "support@example.com", "org_magic": "%s"}`, strings.Repeat("1", 257))),
	}, http.StatusUnprocessableEntity)
	require.Contains(t, extractServerErrorText(r.Body), "MAGIC_INVALID")

	// Invalid flag combo
	r = s.Do("POST", "/api/latest/fleet/enrollment_profiles/automatic", createMDMAppleSetupAssistantRequest{
		TeamID:            nil,
		Name:              "profile_name_missing",
		EnrollmentProfile: json.RawMessage(`{"profile_name": "prof_name", "configuration_web_url": "https://example.com", "department": "foo", "support_phone_number": "555-123-4567", "support_email_address": "support@example.com", "org_magic": "1", "is_mdm_removable": false, "is_supervised": false}`),
	}, http.StatusUnprocessableEntity)
	require.Contains(t, extractServerErrorText(r.Body), "FLAGS_INVALID")

	// create a setup assistant for no team
	noTeamProf := fmt.Sprintf(defaultProf, "no-team")
	s.DoJSON("POST", "/api/latest/fleet/enrollment_profiles/automatic", createMDMAppleSetupAssistantRequest{
		TeamID:            nil,
		Name:              "no-team",
		EnrollmentProfile: json.RawMessage(noTeamProf),
	}, http.StatusOK, &createResp)
	noTeamAsst := createResp.MDMAppleSetupAssistant
	require.Nil(t, noTeamAsst.TeamID)
	require.NotZero(t, noTeamAsst.UploadedAt)
	require.Equal(t, "no-team", noTeamAsst.Name)
	require.JSONEq(t, noTeamProf, string(noTeamAsst.Profile))
	s.lastActivityMatches(fleet.ActivityTypeChangedMacosSetupAssistant{}.ActivityName(),
		`{"name": "no-team", "team_id": null, "team_name": null}`, 0)

	// create a team and a setup assistant for that team
	tm, err := s.ds.NewTeam(ctx, &fleet.Team{
		Name:        t.Name(),
		Description: "desc",
	})
	require.NoError(t, err)
	tmProf := fmt.Sprintf(defaultProf, "team1")
	s.DoJSON("POST", "/api/latest/fleet/enrollment_profiles/automatic", createMDMAppleSetupAssistantRequest{
		TeamID:            &tm.ID,
		Name:              "team1",
		EnrollmentProfile: json.RawMessage(tmProf),
	}, http.StatusOK, &createResp)
	tmAsst := createResp.MDMAppleSetupAssistant
	require.NotNil(t, tmAsst.TeamID)
	require.Equal(t, tm.ID, *tmAsst.TeamID)
	require.NotZero(t, tmAsst.UploadedAt)
	require.Equal(t, "team1", tmAsst.Name)
	require.JSONEq(t, tmProf, string(tmAsst.Profile))
	s.lastActivityMatches(fleet.ActivityTypeChangedMacosSetupAssistant{}.ActivityName(),
		fmt.Sprintf(`{"name": "team1", "team_id": %d, "team_name": %q}`, tm.ID, tm.Name), 0)

	// update no-team
	noTeamProf = fmt.Sprintf(defaultProf, "no-team2")
	s.DoJSON("POST", "/api/latest/fleet/enrollment_profiles/automatic", createMDMAppleSetupAssistantRequest{
		TeamID:            nil,
		Name:              "no-team2",
		EnrollmentProfile: json.RawMessage(noTeamProf),
	}, http.StatusOK, &createResp)
	s.lastActivityMatches(fleet.ActivityTypeChangedMacosSetupAssistant{}.ActivityName(),
		`{"name": "no-team2", "team_id": null, "team_name": null}`, 0)

	// update team
	tmProf = fmt.Sprintf(defaultProf, "team2")
	s.DoJSON("POST", "/api/latest/fleet/enrollment_profiles/automatic", createMDMAppleSetupAssistantRequest{
		TeamID:            &tm.ID,
		Name:              "team2",
		EnrollmentProfile: json.RawMessage(tmProf),
	}, http.StatusOK, &createResp)
	lastChangedActID := s.lastActivityMatches(fleet.ActivityTypeChangedMacosSetupAssistant{}.ActivityName(),
		fmt.Sprintf(`{"name": "team2", "team_id": %d, "team_name": %q}`, tm.ID, tm.Name), 0)

	// sleep a second so the uploaded-at timestamp would change if there were
	// changes, then update again no team/team but without any change, doesn't
	// create a changed activity.
	time.Sleep(time.Second)

	// no change to no-team
	s.DoJSON("POST", "/api/latest/fleet/enrollment_profiles/automatic", createMDMAppleSetupAssistantRequest{
		TeamID:            nil,
		Name:              "no-team2",
		EnrollmentProfile: json.RawMessage(noTeamProf),
	}, http.StatusOK, &createResp)
	// the last activity is that of the team (i.e. no new activity was created for no-team)
	s.lastActivityMatches(fleet.ActivityTypeChangedMacosSetupAssistant{}.ActivityName(),
		fmt.Sprintf(`{"name": "team2", "team_id": %d, "team_name": %q}`, tm.ID, tm.Name), lastChangedActID)

	// no change to team
	s.DoJSON("POST", "/api/latest/fleet/enrollment_profiles/automatic", createMDMAppleSetupAssistantRequest{
		TeamID:            &tm.ID,
		Name:              "team2",
		EnrollmentProfile: json.RawMessage(tmProf),
	}, http.StatusOK, &createResp)
	s.lastActivityMatches(fleet.ActivityTypeChangedMacosSetupAssistant{}.ActivityName(),
		fmt.Sprintf(`{"name": "team2", "team_id": %d, "team_name": %q}`, tm.ID, tm.Name), lastChangedActID)

	// update team with only a setup assistant JSON change, should detect it
	// and create a new activity (name is the same)
	tmProf = fmt.Sprintf(defaultProf, "update")
	s.DoJSON("POST", "/api/latest/fleet/enrollment_profiles/automatic", createMDMAppleSetupAssistantRequest{
		TeamID:            &tm.ID,
		Name:              "team2",
		EnrollmentProfile: json.RawMessage(tmProf),
	}, http.StatusOK, &createResp)
	latestChangedActID := s.lastActivityMatches(fleet.ActivityTypeChangedMacosSetupAssistant{}.ActivityName(),
		fmt.Sprintf(`{"name": "team2", "team_id": %d, "team_name": %q}`, tm.ID, tm.Name), 0)
	require.Greater(t, latestChangedActID, lastChangedActID)

	// get no team
	s.DoJSON("GET", "/api/latest/fleet/enrollment_profiles/automatic", nil, http.StatusOK, &getResp)
	require.Nil(t, getResp.TeamID)
	require.NotZero(t, getResp.UploadedAt)
	require.Equal(t, "no-team2", getResp.Name)
	require.JSONEq(t, noTeamProf, string(getResp.Profile))

	// get team
	s.DoJSON("GET", "/api/latest/fleet/enrollment_profiles/automatic", nil, http.StatusOK, &getResp, "team_id", fmt.Sprint(tm.ID))
	require.NotNil(t, getResp.TeamID)
	require.Equal(t, tm.ID, *getResp.TeamID)
	require.NotZero(t, getResp.UploadedAt)
	require.Equal(t, "team2", getResp.Name)
	require.JSONEq(t, tmProf, string(getResp.Profile))

	// try to set the url
	tmProf = `{"url": "https://example.com"}`
	res := s.Do("POST", "/api/latest/fleet/enrollment_profiles/automatic", createMDMAppleSetupAssistantRequest{
		TeamID:            &tm.ID,
		Name:              "team5",
		EnrollmentProfile: json.RawMessage(tmProf),
	}, http.StatusUnprocessableEntity)
	errMsg := extractServerErrorText(res.Body)
	require.Contains(t, errMsg, `The automatic enrollment profile can't include url.`)
	s.lastActivityMatches(fleet.ActivityTypeChangedMacosSetupAssistant{}.ActivityName(),
		fmt.Sprintf(`{"name": "team2", "team_id": %d, "team_name": %q}`, tm.ID, tm.Name), latestChangedActID)

	// try to set a non-object json value
	tmProf = `true`
	res = s.Do("POST", "/api/latest/fleet/enrollment_profiles/automatic", createMDMAppleSetupAssistantRequest{
		TeamID:            &tm.ID,
		Name:              "team6",
		EnrollmentProfile: json.RawMessage(tmProf),
	}, http.StatusInternalServerError) // TODO: that should be a 4xx error, see #4406
	errMsg = extractServerErrorText(res.Body)
	require.Contains(t, errMsg, `cannot unmarshal bool into Go value of type map[string]interface`)
	s.lastActivityMatches(fleet.ActivityTypeChangedMacosSetupAssistant{}.ActivityName(),
		fmt.Sprintf(`{"name": "team2", "team_id": %d, "team_name": %q}`, tm.ID, tm.Name), latestChangedActID)

	// delete the no-team setup assistant
	s.Do("DELETE", "/api/latest/fleet/enrollment_profiles/automatic", nil, http.StatusNoContent)
	latestChangedActID = s.lastActivityMatches(fleet.ActivityTypeDeletedMacosSetupAssistant{}.ActivityName(),
		`{"name": "no-team2", "team_id": null, "team_name": null}`, 0)

	// get for no team returns 404
	s.DoJSON("GET", "/api/latest/fleet/enrollment_profiles/automatic", nil, http.StatusNotFound, &getResp)

	// delete the team (not the assistant), this also deletes the assistant
	err = s.ds.DeleteTeam(ctx, tm.ID)
	require.NoError(t, err)

	// get for team returns 404
	s.DoJSON("GET", "/api/latest/fleet/enrollment_profiles/automatic", nil, http.StatusNotFound, &getResp, "team_id", fmt.Sprint(tm.ID))

	// no deleted activity was created for the team as the whole team was deleted
	// (a deleted team activity would exist if that was done via the API and not
	// directly with the datastore)
	s.lastActivityMatches(fleet.ActivityTypeDeletedMacosSetupAssistant{}.ActivityName(),
		`{"name": "no-team2", "team_id": null, "team_name": null}`, latestChangedActID)

	// create another team and a setup assistant for that team
	tm2, err := s.ds.NewTeam(ctx, &fleet.Team{
		Name:        t.Name() + "2",
		Description: "desc2",
	})
	require.NoError(t, err)
	tm2Prof := fmt.Sprintf(defaultProf, "teamB")
	s.DoJSON("POST", "/api/latest/fleet/enrollment_profiles/automatic", createMDMAppleSetupAssistantRequest{
		TeamID:            &tm2.ID,
		Name:              "teamB",
		EnrollmentProfile: json.RawMessage(tm2Prof),
	}, http.StatusOK, &createResp)
	s.lastActivityMatches(fleet.ActivityTypeChangedMacosSetupAssistant{}.ActivityName(),
		fmt.Sprintf(`{"name": "teamB", "team_id": %d, "team_name": %q}`, tm2.ID, tm2.Name), 0)

	// delete that team's setup assistant
	s.Do("DELETE", "/api/latest/fleet/enrollment_profiles/automatic", nil, http.StatusNoContent, "team_id", fmt.Sprint(tm2.ID))
	s.lastActivityMatches(fleet.ActivityTypeDeletedMacosSetupAssistant{}.ActivityName(),
		fmt.Sprintf(`{"name": "teamB", "team_id": %d, "team_name": %q}`, tm2.ID, tm2.Name), 0)

	// Try with a team that has no relevant ABM tokens
	teamNoABM, err := s.ds.NewTeam(ctx, &fleet.Team{
		Name:        t.Name() + "no_abm",
		Description: "no abm",
	})
	require.NoError(t, err)
	// Adding another, unrelated token to the DB means that this team (which has no hosts and is not
	// a default team for any token) will not have any relevant tokens and thus we don't know which
	// token to use to hit the Apple APIs.
	otherOrg := t.Name() + "some_other_org"
	s.enableABM(otherOrg)
	// mysql.CreateABMKeyCertIfNotExists(t, s.ds)
	// mysql.CreateAndSetABMToken(t, s.ds, "nurv")
	// err = s.depStorage.StoreConfig(ctx, "nurv", &nanodep_client.Config{BaseURL: srv.URL})
	s.mockDEPResponse(otherOrg, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		encoder := json.NewEncoder(w)
		switch r.URL.Path {
		case "/session":
			_, _ = w.Write([]byte(`{"auth_session_token": "session123"}`))
		case "/account":
			_, _ = w.Write([]byte(fmt.Sprintf(`{"admin_id": "admin123", "org_name": "%s"}`, "foo")))
		case "/profile":
			w.WriteHeader(http.StatusOK)
			require.NoError(t, encoder.Encode(godep.ProfileResponse{ProfileUUID: "profile123"}))
		}
	}))
	require.NoError(t, err)
	r = s.Do("POST", "/api/latest/fleet/enrollment_profiles/automatic", createMDMAppleSetupAssistantRequest{
		TeamID:            &teamNoABM.ID,
		Name:              "profile_name_missing",
		EnrollmentProfile: json.RawMessage(fmt.Sprintf(defaultProf, "no_abm")),
	}, http.StatusUnprocessableEntity)
	require.Contains(t, extractServerErrorText(r.Body), "No relevant ABM tokens found. Please set this team as a default team for an ABM token.")
}

// only asserts the profile identifier, status and operation (per host)
func (s *integrationMDMTestSuite) assertHostAppleConfigProfiles(want map[*fleet.Host][]fleet.HostMDMAppleProfile) {
	t := s.T()
	ds := s.ds
	ctx := context.Background()

	for h, wantProfs := range want {
		gotProfs, err := ds.GetHostMDMAppleProfiles(ctx, h.UUID)
		require.NoError(t, err)
		idents := make([]string, 0, len(gotProfs))
		for _, gp := range gotProfs {
			idents = append(idents, gp.Identifier)
		}
		require.Equal(t, len(wantProfs), len(gotProfs), "apple host uuid: %s, profiles: %v", h.UUID, idents)

		sort.Slice(gotProfs, func(i, j int) bool {
			l, r := gotProfs[i], gotProfs[j]
			return l.Identifier < r.Identifier
		})
		sort.Slice(wantProfs, func(i, j int) bool {
			l, r := wantProfs[i], wantProfs[j]
			return l.Identifier < r.Identifier
		})
		for i, wp := range wantProfs {
			gp := gotProfs[i]
			require.Equal(t, wp.Identifier, gp.Identifier, "host uuid: %s, prof id: %s", h.UUID, gp.Identifier)
			require.Equal(t, wp.OperationType, gp.OperationType, "host uuid: %s, prof id: %s", h.UUID, gp.Identifier)
			require.Equal(t, wp.Status, gp.Status, "host uuid: %s, prof id: %s", h.UUID, gp.Identifier)
		}
	}
}

// only asserts the profile name, status and operation (per host)
func (s *integrationMDMTestSuite) assertHostWindowsConfigProfiles(want map[*fleet.Host][]fleet.HostMDMWindowsProfile) {
	t := s.T()
	ds := s.ds
	ctx := context.Background()

	for h, wantProfs := range want {
		gotProfs, err := ds.GetHostMDMWindowsProfiles(ctx, h.UUID)
		require.NoError(t, err)
		require.Equal(t, len(wantProfs), len(gotProfs), "host uuid: %s", h.UUID)

		sort.Slice(gotProfs, func(i, j int) bool {
			l, r := gotProfs[i], gotProfs[j]
			return l.Name < r.Name
		})
		sort.Slice(wantProfs, func(i, j int) bool {
			l, r := wantProfs[i], wantProfs[j]
			return l.Name < r.Name
		})
		for i, wp := range wantProfs {
			gp := gotProfs[i]
			require.Equal(t, wp.Name, gp.Name, "host uuid: %s, prof id: %s", h.UUID, gp.Name)
			require.Equal(t, wp.OperationType, gp.OperationType, "host uuid: %s, prof id: %s", h.UUID, gp.Name)
			require.Equal(t, wp.Status, gp.Status, "host uuid: %s, prof id: %s", h.UUID, gp.Name)
		}
	}
}

func (s *integrationMDMTestSuite) assertConfigProfilesByIdentifier(teamID *uint, profileIdent string, exists bool) (profile *fleet.MDMAppleConfigProfile) {
	t := s.T()
	if teamID == nil {
		teamID = ptr.Uint(0)
	}
	var cfgProfs []*fleet.MDMAppleConfigProfile
	mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
		return sqlx.SelectContext(context.Background(), q, &cfgProfs, `SELECT * FROM mdm_apple_configuration_profiles WHERE team_id = ?`, teamID)
	})

	label := "exist"
	if !exists {
		label = "not exist"
	}
	require.Condition(t, func() bool {
		for _, p := range cfgProfs {
			if p.Identifier == profileIdent {
				profile = p
				return exists // success if we want it to exist, failure if we don't
			}
		}
		return !exists
	}, "a config profile must %s with identifier: %s", label, profileIdent)

	return profile
}

func (s *integrationMDMTestSuite) assertMacOSConfigProfilesByName(teamID *uint, profileName string, exists bool) {
	t := s.T()
	if teamID == nil {
		teamID = ptr.Uint(0)
	}
	var cfgProfs []*fleet.MDMAppleConfigProfile
	mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
		return sqlx.SelectContext(context.Background(), q, &cfgProfs, `SELECT name FROM mdm_apple_configuration_profiles WHERE team_id = ?`, teamID)
	})

	label := "exist"
	if !exists {
		label = "not exist"
	}
	require.Condition(t, func() bool {
		for _, p := range cfgProfs {
			if p.Name == profileName {
				return exists // success if we want it to exist, failure if we don't
			}
		}
		return !exists
	}, "a config profile must %s with name: %s", label, profileName)
}

func (s *integrationMDMTestSuite) assertMacOSDeclarationsByName(teamID *uint, declarationName string, exists bool) {
	t := s.T()
	if teamID == nil {
		teamID = ptr.Uint(0)
	}
	var cfgProfs []*fleet.MDMAppleConfigProfile
	mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
		return sqlx.SelectContext(context.Background(), q, &cfgProfs, `SELECT name FROM mdm_apple_declarations WHERE team_id = ?`, teamID)
	})

	label := "exist"
	if !exists {
		label = "not exist"
	}
	require.Condition(t, func() bool {
		for _, p := range cfgProfs {
			if p.Name == declarationName {
				return exists // success if we want it to exist, failure if we don't
			}
		}
		return !exists
	}, "a config profile must %s with name: %s", label, declarationName)
}

func (s *integrationMDMTestSuite) assertWindowsConfigProfilesByName(teamID *uint, profileName string, exists bool) {
	t := s.T()
	if teamID == nil {
		teamID = ptr.Uint(0)
	}
	var cfgProfs []*fleet.MDMWindowsConfigProfile
	mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
		return sqlx.SelectContext(context.Background(), q, &cfgProfs,
			`SELECT profile_uuid, team_id, name, syncml, created_at, uploaded_at FROM mdm_windows_configuration_profiles WHERE team_id = ?`,
			teamID)
	})

	label := "exist"
	if !exists {
		label = "not exist"
	}
	require.Condition(t, func() bool {
		for _, p := range cfgProfs {
			if p.Name == profileName {
				return exists // success if we want it to exist, failure if we don't
			}
		}
		return !exists
	}, "a config profile must %s with name: %s", label, profileName)
}

// generates the body and headers part of a multipart request ready to be
// used via s.DoRawWithHeaders to POST /api/_version_/fleet/mdm/apple/profiles.
func generateNewProfileMultipartRequest(t *testing.T,
	fileName string, fileContent []byte, token string, extraFields map[string][]string,
) (*bytes.Buffer, map[string]string) {
	return generateMultipartRequest(t, "profile", fileName, fileContent, token, extraFields)
}

func generateMultipartRequest(t *testing.T,
	uploadFileField, fileName string, fileContent []byte, token string,
	extraFields map[string][]string,
) (*bytes.Buffer, map[string]string) {
	var body bytes.Buffer

	writer := multipart.NewWriter(&body)

	// add file content
	if fileName != "" || len(fileContent) > 0 {
		ff, err := writer.CreateFormFile(uploadFileField, fileName)
		require.NoError(t, err)
		_, err = io.Copy(ff, bytes.NewReader(fileContent))
		require.NoError(t, err)
	}

	// add extra fields
	for key, values := range extraFields {
		for _, value := range values {
			err := writer.WriteField(key, value)
			require.NoError(t, err)
		}
	}

	err := writer.Close()
	require.NoError(t, err)

	headers := map[string]string{
		"Content-Type":  writer.FormDataContentType(),
		"Accept":        "application/json",
		"Authorization": fmt.Sprintf("Bearer %s", token),
	}
	return &body, headers
}

func (s *integrationMDMTestSuite) uploadBootstrapPackage(
	pkg *fleet.MDMAppleBootstrapPackage,
	expectedStatus int,
	wantErr string,
	dryRun bool,
) {
	t := s.T()

	var b bytes.Buffer
	w := multipart.NewWriter(&b)

	// add the package field
	fw, err := w.CreateFormFile("package", pkg.Name)
	require.NoError(t, err)
	_, err = io.Copy(fw, bytes.NewBuffer(pkg.Bytes))
	require.NoError(t, err)

	// add the team_id field
	err = w.WriteField("team_id", fmt.Sprint(pkg.TeamID))
	require.NoError(t, err)

	w.Close()

	headers := map[string]string{
		"Content-Type":  w.FormDataContentType(),
		"Accept":        "application/json",
		"Authorization": fmt.Sprintf("Bearer %s", s.token),
	}

	res := s.DoRawWithHeaders("POST", "/api/latest/fleet/bootstrap", b.Bytes(), expectedStatus, headers, "dry_run", strconv.FormatBool(dryRun))

	if wantErr != "" {
		errMsg := extractServerErrorText(res.Body)
		assert.Contains(t, errMsg, wantErr)
	}
}

func (s *integrationMDMTestSuite) uploadEULA(
	eula *fleet.MDMEULA,
	expectedStatus int,
	wantErr string,
) {
	t := s.T()

	var b bytes.Buffer
	w := multipart.NewWriter(&b)

	// add the eula field
	fw, err := w.CreateFormFile("eula", eula.Name)
	require.NoError(t, err)
	_, err = io.Copy(fw, bytes.NewBuffer(eula.Bytes))
	require.NoError(t, err)
	w.Close()

	headers := map[string]string{
		"Content-Type":  w.FormDataContentType(),
		"Accept":        "application/json",
		"Authorization": fmt.Sprintf("Bearer %s", s.token),
	}

	res := s.DoRawWithHeaders("POST", "/api/latest/fleet/setup_experience/eula", b.Bytes(), expectedStatus, headers)

	if wantErr != "" {
		errMsg := extractServerErrorText(res.Body)
		assert.Contains(t, errMsg, wantErr)
	}
}

// TestGitOpsUserActions tests the MDM permissions listed in ../../docs/Using\ Fleet/manage-access.md
func (s *integrationMDMTestSuite) TestGitOpsUserActions() {
	t := s.T()
	ctx := context.Background()

	//
	// Setup test data.
	// All setup actions are authored by a global admin.
	//

	t1, err := s.ds.NewTeam(ctx, &fleet.Team{
		Name: "Foo",
	})
	require.NoError(t, err)
	t2, err := s.ds.NewTeam(ctx, &fleet.Team{
		Name: "Bar",
	})
	require.NoError(t, err)
	t3, err := s.ds.NewTeam(ctx, &fleet.Team{
		Name: "Zoo",
	})
	require.NoError(t, err)
	// Create the global GitOps user we'll use in tests.
	u := &fleet.User{
		Name:       "GitOps",
		Email:      "gitops1-mdm@example.com",
		GlobalRole: ptr.String(fleet.RoleGitOps),
	}
	require.NoError(t, u.SetPassword(test.GoodPassword, 10, 10))
	_, err = s.ds.NewUser(context.Background(), u)
	require.NoError(t, err)
	// Create a GitOps user for team t1 we'll use in tests.
	u2 := &fleet.User{
		Name:       "GitOps 2",
		Email:      "gitops2-mdm@example.com",
		GlobalRole: nil,
		Teams: []fleet.UserTeam{
			{
				Team: *t1,
				Role: fleet.RoleGitOps,
			},
			{
				Team: *t3,
				Role: fleet.RoleGitOps,
			},
		},
	}
	require.NoError(t, u2.SetPassword(test.GoodPassword, 10, 10))
	_, err = s.ds.NewUser(context.Background(), u2)
	require.NoError(t, err)

	//
	// Start running permission tests with user gitops1-mdm.
	//
	s.setTokenForTest(t, "gitops1-mdm@example.com", test.GoodPassword)

	// Attempt to edit global MDM settings, should allow (also ensure the IdP settings are cleared).
	acResp := appConfigResponse{}
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
		"mdm": {
			"macos_setup": {
				"enable_end_user_authentication": false
			},
			"enable_disk_encryption": true,
			"end_user_authentication": {
				"entity_id": "",
				"idp_name": "",
				"metadata_url": ""
			}
		}
  }`), http.StatusOK, &acResp)
	assert.True(t, acResp.MDM.EnableDiskEncryption.Value)

	// Attempt to setup Apple MDM, will fail but the important thing is that it
	// fails with 422 (cannot enable end user auth because no IdP is configured)
	// and not 403 forbidden.
	s.Do("PATCH", "/api/latest/fleet/setup_experience",
		fleet.MDMAppleSetupPayload{TeamID: ptr.Uint(0), EnableEndUserAuthentication: ptr.Bool(true)}, http.StatusUnprocessableEntity)

	// Attempt to update the Apple MDM settings but with no change, just to
	// validate the access.
	s.Do("PATCH", "/api/latest/fleet/mdm/apple/settings",
		fleet.MDMAppleSettingsPayload{}, http.StatusNoContent)

	// Attempt to set profile batch globally, should allow.
	globalProfiles := [][]byte{
		mobileconfigForTest("N1", "I1"),
		mobileconfigForTest("N2", "I2"),
	}
	s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: globalProfiles}, http.StatusNoContent)

	// Attempt to edit team MDM settings, should allow.
	teamSpecs := applyTeamSpecsRequest{Specs: []*fleet.TeamSpec{{
		Name: t1.Name,
		MDM: fleet.TeamSpecMDM{
			EnableDiskEncryption: optjson.SetBool(true),
			MacOSSettings: map[string]interface{}{
				"custom_settings": []interface{}{"foo", "bar"},
			},
		},
	}}}
	s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK)

	// Attempt to set profile batch for team t1, should allow.
	teamProfiles := [][]byte{
		mobileconfigForTest("N3", "I3"),
		mobileconfigForTest("N4", "I4"),
	}
	s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{
		Profiles: teamProfiles,
	}, http.StatusNoContent, "team_id", fmt.Sprint(t1.ID))

	//
	// Start running permission tests with user gitops2-mdm,
	// which is GitOps for teams t1 and t3.
	//
	s.setTokenForTest(t, "gitops2-mdm@example.com", test.GoodPassword)

	// Attempt to edit team t1 MDM settings, should allow.
	teamSpecs = applyTeamSpecsRequest{Specs: []*fleet.TeamSpec{{
		Name: t1.Name,
		MDM: fleet.TeamSpecMDM{
			EnableDiskEncryption: optjson.SetBool(true),
			MacOSSettings: map[string]interface{}{
				"custom_settings": []interface{}{"foo", "bar"},
			},
		},
	}}}
	s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK)

	// Attempt to set profile batch for team t1, should allow.
	teamProfiles = [][]byte{
		mobileconfigForTest("N5", "I5"),
		mobileconfigForTest("N6", "I6"),
	}
	s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{
		Profiles: teamProfiles,
	}, http.StatusNoContent, "team_id", fmt.Sprint(t1.ID))

	// Attempt to set profile batch for team t2, should not allow.
	teamProfiles = [][]byte{
		mobileconfigForTest("N7", "I7"),
		mobileconfigForTest("N8", "I8"),
	}
	s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{
		Profiles: teamProfiles,
	}, http.StatusForbidden, "team_id", fmt.Sprint(t2.ID))

	// Attempt to retrieve host profiles fails if the host doesn't belong to the team
	h1, err := s.ds.NewHost(ctx, &fleet.Host{
		NodeKey:  ptr.String(t.Name() + "1"),
		UUID:     t.Name() + "1",
		Hostname: t.Name() + "foo.local",
	})
	require.NoError(t, err)
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/mdm/hosts/%d/profiles", h1.ID), getHostRequest{}, http.StatusForbidden, &getHostResponse{})

	err = s.ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&t1.ID, []uint{h1.ID}))
	require.NoError(t, err)
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/mdm/hosts/%d/profiles", h1.ID), getHostRequest{}, http.StatusOK, &getHostResponse{})
}

func (s *integrationMDMTestSuite) TestOrgLogo() {
	t := s.T()

	// change org logo urls
	var acResp appConfigResponse
	s.DoJSON("PATCH", "/api/v1/fleet/config", json.RawMessage(`{
		"org_info": {
			"org_logo_url": "http://test-image.com",
			"org_logo_url_light_background": "http://test-image-light.com"
		}
	}`), http.StatusOK, &acResp)

	// enroll a host
	token := "token_test_migration"
	host := createOrbitEnrolledHost(t, "darwin", "h", s.ds)
	createDeviceTokenForHost(t, s.ds, host.ID, token)

	// check icon urls are correct
	getDesktopResp := fleetDesktopResponse{}
	res := s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token+"/desktop", nil, http.StatusOK)
	require.NoError(t, json.NewDecoder(res.Body).Decode(&getDesktopResp))
	require.NoError(t, res.Body.Close())
	require.NoError(t, getDesktopResp.Err)
	require.Equal(t, acResp.OrgInfo.OrgLogoURL, getDesktopResp.Config.OrgInfo.OrgLogoURL)
	require.Equal(t, acResp.OrgInfo.OrgLogoURLLightBackground, getDesktopResp.Config.OrgInfo.OrgLogoURLLightBackground)
}

func (s *integrationMDMTestSuite) setTokenForTest(t *testing.T, email, password string) {
	oldToken := s.token
	t.Cleanup(func() {
		s.token = oldToken
	})

	s.token = s.getCachedUserToken(email, password)
}

func (s *integrationMDMTestSuite) TestSSO() {
	t := s.T()

	lastSubmittedProfile := &godep.Profile{}
	mdmDevice, wantSettings := s.setUpEndUserAuthentication(t, lastSubmittedProfile)
	di, err := mdmtest.EncodeDeviceInfo(fleet.MDMAppleMachineInfo{
		Serial: mdmDevice.SerialNumber,
		UDID:   mdmDevice.UUID,
	})
	require.NoError(t, err)

	// patch without specifying the mdm sso settings fields and an unrelated
	// field, should not remove them
	acResp := appConfigResponse{}
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
		"mdm": { "enable_disk_encryption": true }
  }`), http.StatusOK, &acResp)
	assert.Equal(t, wantSettings, acResp.MDM.EndUserAuthentication.SSOProviderSettings)

	s.runWorker()

	// patch with explicitly empty mdm sso settings fields, would remove
	// them but this is a dry-run
	acResp = appConfigResponse{}
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
		"mdm": {
			"end_user_authentication": {
				"entity_id": "",
				"idp_name": "",
				"metadata_url": ""
			},
			"macos_setup": {
				"enable_end_user_authentication": false
			}
		}
	}`), http.StatusOK, &acResp, "dry_run", "true")
	assert.Equal(t, wantSettings, acResp.MDM.EndUserAuthentication.SSOProviderSettings)

	s.runWorker()

	// patch with explicitly empty mdm sso settings fields, fails because end user auth is still enabled
	acResp = appConfigResponse{}
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
		"mdm": {
			"end_user_authentication": {
				"entity_id": "",
				"idp_name": "",
				"metadata_url": ""
			}
		}
	}`), http.StatusUnprocessableEntity, &acResp)

	// patch with explicitly empty mdm sso settings fields and disabled end user auth, removes them
	acResp = appConfigResponse{}
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
		"mdm": {
			"end_user_authentication": {
				"entity_id": "",
				"idp_name": "",
				"metadata_url": ""
			},
			"macos_setup": {
				"enable_end_user_authentication": false
			}
		}
	}`), http.StatusOK, &acResp)
	assert.Empty(t, acResp.MDM.EndUserAuthentication.SSOProviderSettings)

	s.runWorker()
	assert.Empty(t, lastSubmittedProfile.ConfigurationWebURL)
	assert.NotEmpty(t, lastSubmittedProfile.URL)

	// "mdm.test.com" entity ID is defined in `tools/saml/config.php`.
	acResp = appConfigResponse{}
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
		"server_settings": {
			"server_url": "https://localhost:8080"
		},
		"mdm": {
			"end_user_authentication": {
				"entity_id": "mdm.test.com",
				"idp_name": "SimpleSAML",
				"metadata_url": "http://localhost:9080/simplesaml/saml2/idp/metadata.php"
			},
			"macos_setup": {
				"enable_end_user_authentication": true
			}
		}
	}`), http.StatusOK, &acResp)

	s.runWorker()
	require.Contains(t, lastSubmittedProfile.URL, acResp.ServerSettings.ServerURL+"/mdm/sso")
	require.Equal(t, acResp.ServerSettings.ServerURL+"/mdm/sso", lastSubmittedProfile.ConfigurationWebURL)

	res := s.LoginMDMSSOUser("sso_user", "user123#")
	require.NotEmpty(t, res.Header.Get("Location"))
	require.Equal(t, http.StatusSeeOther, res.StatusCode)

	u, err := url.Parse(res.Header.Get("Location"))
	require.NoError(t, err)
	q := u.Query()
	user1EnrollRef := q.Get("enrollment_reference")
	// without an EULA uploaded
	require.False(t, q.Has("eula_token"))
	require.True(t, q.Has("profile_token"))
	require.True(t, q.Has("enrollment_reference"))
	require.False(t, q.Has("error"))
	// the url retrieves a valid profile
	s.downloadAndVerifyEnrollmentProfile(t, optsDownloadEnrollProf{
		basePath:  "/api/mdm/apple/enroll",
		enrollRef: q.Get("enrollment_reference"),
		token:     q.Get("profile_token"),
		diParam:   di,
	})

	// IdP info stored is accurate for the account
	s.checkStoredIdPInfo(t, user1EnrollRef, "sso_user", "SSO User 1", "sso_user@example.com")

	res = s.LoginMDMSSOUser("sso_user", "user123#")
	require.NotEmpty(t, res.Header.Get("Location"))
	require.Equal(t, http.StatusSeeOther, res.StatusCode)

	u, err = url.Parse(res.Header.Get("Location"))
	require.NoError(t, err)
	q = u.Query()
	user1EnrollRef = q.Get("enrollment_reference")
	// without an EULA uploaded
	require.False(t, q.Has("eula_token"))
	require.True(t, q.Has("profile_token"))
	require.True(t, q.Has("enrollment_reference"))
	require.False(t, q.Has("error"))
	// the url retrieves a valid profile
	s.downloadAndVerifyEnrollmentProfile(t, optsDownloadEnrollProf{
		basePath:  "/api/mdm/apple/enroll",
		enrollRef: q.Get("enrollment_reference"),
		token:     q.Get("profile_token"),
		diParam:   di,
	})

	// IdP info stored is accurate for the account
	s.checkStoredIdPInfo(t, user1EnrollRef, "sso_user", "SSO User 1", "sso_user@example.com")

	res = s.LoginMDMSSOUser("sso_user", "user123#")
	require.NotEmpty(t, res.Header.Get("Location"))
	require.Equal(t, http.StatusSeeOther, res.StatusCode)

	u, err = url.Parse(res.Header.Get("Location"))
	require.NoError(t, err)
	q = u.Query()
	user1EnrollRef = q.Get("enrollment_reference")

	// upload an EULA
	pdfBytes := []byte("%PDF-1.pdf-contents")
	pdfName := "eula.pdf"
	s.uploadEULA(&fleet.MDMEULA{Bytes: pdfBytes, Name: pdfName}, http.StatusOK, "")

	res = s.LoginMDMSSOUser("sso_user", "user123#")
	require.NotEmpty(t, res.Header.Get("Location"))
	require.Equal(t, http.StatusSeeOther, res.StatusCode)
	u, err = url.Parse(res.Header.Get("Location"))
	require.NoError(t, err)
	q = u.Query()
	// with an EULA uploaded, all values are present
	require.True(t, q.Has("eula_token"))
	require.True(t, q.Has("profile_token"))
	require.True(t, q.Has("enrollment_reference"))
	require.False(t, q.Has("error"))
	// the enrollment reference is the same for the same user
	require.Equal(t, user1EnrollRef, q.Get("enrollment_reference"))
	// the url retrieves a valid profile
	prof := s.downloadAndVerifyEnrollmentProfile(t, optsDownloadEnrollProf{
		basePath:  "/api/mdm/apple/enroll",
		enrollRef: user1EnrollRef,
		token:     q.Get("profile_token"),
		diParam:   di,
	})

	// the url retrieves a valid EULA
	resp := s.DoRaw("GET", "/api/latest/fleet/setup_experience/eula/"+q.Get("eula_token"), nil, http.StatusOK)
	require.EqualValues(t, len(pdfBytes), resp.ContentLength)
	require.Equal(t, "application/pdf", resp.Header.Get("content-type"))
	respBytes, err := io.ReadAll(resp.Body)
	require.NoError(t, err)
	require.EqualValues(t, pdfBytes, respBytes)

	// IdP info stored is accurate for the account
	s.checkStoredIdPInfo(t, user1EnrollRef, "sso_user", "SSO User 1", "sso_user@example.com")

	enrollURL := ""
	scepURL := ""
	for _, p := range prof.PayloadContent {
		switch p.PayloadType {
		case "com.apple.security.scep":
			scepURL = p.PayloadContent.URL
		case "com.apple.mdm":
			enrollURL = p.ServerURL
		}
	}
	require.NotEmpty(t, enrollURL)
	require.NotEmpty(t, scepURL)

	// enroll the device using the provided profile
	// we're using localhost for SSO because that's how the local
	// SimpleSAML server is configured, and s.server.URL changes between
	// test runs.
	mdmDevice.EnrollInfo.MDMURL = strings.Replace(enrollURL, "https://localhost:8080", s.server.URL, 1)
	mdmDevice.EnrollInfo.SCEPURL = strings.Replace(scepURL, "https://localhost:8080", s.server.URL, 1)
	err = mdmDevice.Enroll()
	require.NoError(t, err)

	// Enroll generated the TokenUpdate request to Fleet and enqueued the
	// Post-DEP enrollment job, it needs to be processed.
	s.runWorker()

	// ask for commands and verify that we get AccountConfiguration
	var accCmd *mdm.Command
	cmd, err := mdmDevice.Idle()
	require.NoError(t, err)
	for cmd != nil {
		if cmd.Command.RequestType == "AccountConfiguration" {
			accCmd = cmd
		}
		cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID)
		require.NoError(t, err)
	}
	require.NotNil(t, accCmd)
	require.NotNil(t, accCmd.Command)

	var fullAccCmd *micromdm.CommandPayload
	require.NoError(t, plist.Unmarshal(accCmd.Raw, &fullAccCmd))
	require.True(t, fullAccCmd.Command.AccountConfiguration.LockPrimaryAccountInfo)
	require.Equal(t, "SSO User 1", fullAccCmd.Command.AccountConfiguration.PrimaryAccountFullName)
	require.Equal(t, "sso_user", fullAccCmd.Command.AccountConfiguration.PrimaryAccountUserName)

	// report host details for the device
	var hostResp getHostResponse
	s.DoJSON("GET", "/api/v1/fleet/hosts/identifier/"+mdmDevice.UUID, nil, http.StatusOK, &hostResp)
	assert.Len(t, hostResp.Host.EndUsers, 1)
	assert.Equal(t, "", hostResp.Host.EndUsers[0].IdpFullName)                     // full name is not recorded for mdm flow until scim sync
	assert.Equal(t, "sso_user@example.com", hostResp.Host.EndUsers[0].IdpUserName) // email is the same as the one used to enroll
	hostID := hostResp.Host.ID
	hostResp = getHostResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/hosts/%d", hostID), nil, http.StatusOK, &hostResp)
	assert.Len(t, hostResp.Host.EndUsers, 1)
	assert.Equal(t, "", hostResp.Host.EndUsers[0].IdpFullName)                     // full name is not recorded for mdm flow until scim sync
	assert.Equal(t, "sso_user@example.com", hostResp.Host.EndUsers[0].IdpUserName) // email is the same as the one used to enroll

	ac, err := s.ds.AppConfig(context.Background())
	require.NoError(t, err)

	detailQueries := osquery_utils.GetDetailQueries(context.Background(), config.FleetConfig{}, ac, &ac.Features, osquery_utils.Integrations{}, nil)

	// simulate osquery reporting mdm information
	rows := []map[string]string{
		{
			"enrolled":           "true",
			"installed_from_dep": "true",
			"server_url":         "https://test.example.com?enrollment_reference=" + user1EnrollRef,
			"payload_identifier": apple_mdm.FleetPayloadIdentifier,
		},
	}
	err = detailQueries["mdm"].DirectIngestFunc(
		context.Background(),
		kitlog.NewNopLogger(),
		&fleet.Host{ID: hostResp.Host.ID},
		s.ds,
		rows,
	)
	require.NoError(t, err)

	hostResp = getHostResponse{}
	s.DoJSON("GET", "/api/v1/fleet/hosts/identifier/"+mdmDevice.UUID, nil, http.StatusOK, &hostResp)
	checkEndUser := func() {
		require.Len(t, hostResp.Host.EndUsers, 1)
		endUser := hostResp.Host.EndUsers[0]
		assert.Equal(t, "sso_user@example.com", endUser.IdpUserName)
		assert.Nil(t, endUser.IdpInfoUpdatedAt)
		assert.Empty(t, endUser.IdpID)
		assert.Empty(t, endUser.OtherEmails)
		assert.Empty(t, endUser.IdpFullName)
		assert.Empty(t, endUser.IdpGroups)
	}
	checkEndUser()
	hostResp = getHostResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/hosts/%d", hostID), nil, http.StatusOK, &hostResp)
	checkEndUser()

	// sumulate osquery reporting chrome extension information
	rows = []map[string]string{
		{"email": "g1@example.com"},
		{"email": "g2@example.com"},
	}
	err = detailQueries["google_chrome_profiles"].DirectIngestFunc(
		context.Background(),
		kitlog.NewNopLogger(),
		&fleet.Host{ID: hostResp.Host.ID},
		s.ds,
		rows,
	)
	require.NoError(t, err)

	hostResp = getHostResponse{}
	s.DoJSON("GET", "/api/v1/fleet/hosts/identifier/"+mdmDevice.UUID, nil, http.StatusOK, &hostResp)
	checkEndUser = func() {
		require.Len(t, hostResp.Host.EndUsers, 1)
		endUser := hostResp.Host.EndUsers[0]
		assert.Equal(t, "sso_user@example.com", endUser.IdpUserName)
		assert.Nil(t, endUser.IdpInfoUpdatedAt)
		assert.Empty(t, endUser.IdpID)
		assert.Empty(t, endUser.IdpFullName)
		assert.Empty(t, endUser.IdpGroups)
		require.Len(t, endUser.OtherEmails, 2)
		othersByEmail := make(map[string]string, 2)
		for _, otherEmail := range endUser.OtherEmails {
			othersByEmail[otherEmail.Email] = otherEmail.Source
		}
		source, ok := othersByEmail["g1@example.com"]
		require.True(t, ok)
		require.Equal(t, fleet.DeviceMappingGoogleChromeProfiles, source)
		source, ok = othersByEmail["g2@example.com"]
		require.True(t, ok)
		require.Equal(t, fleet.DeviceMappingGoogleChromeProfiles, source)
	}
	checkEndUser()
	hostResp = getHostResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/hosts/%d", hostID), nil, http.StatusOK, &hostResp)
	checkEndUser()

	// host device mapping includes the SSO user and the chrome extension users
	var dmResp listHostDeviceMappingResponse
	s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/hosts/%d/device_mapping", hostResp.Host.ID), nil, http.StatusOK, &dmResp)
	require.Len(t, dmResp.DeviceMapping, 3)
	sourceByEmail := make(map[string]string, 3)
	for _, dm := range dmResp.DeviceMapping {
		sourceByEmail[dm.Email] = dm.Source
	}
	source, ok := sourceByEmail["sso_user@example.com"]
	require.True(t, ok)
	require.Equal(t, fleet.DeviceMappingMDMIdpAccounts, source)
	source, ok = sourceByEmail["g1@example.com"]
	require.True(t, ok)
	require.Equal(t, "google_chrome_profiles", source)
	source, ok = sourceByEmail["g2@example.com"]
	require.True(t, ok)
	require.Equal(t, "google_chrome_profiles", source)

	// list hosts can filter on mdm idp email
	var hostsResp listHostsResponse
	s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/hosts?query=%s&device_mapping=true", url.QueryEscape("sso_user@example.com")), nil, http.StatusOK, &hostsResp)
	require.Len(t, hostsResp.Hosts, 1)
	gotHost := hostsResp.Hosts[0]
	require.Equal(t, hostResp.Host.ID, gotHost.ID)
	require.NotNil(t, gotHost.DeviceMapping)
	var dm []fleet.HostDeviceMapping
	require.NoError(t, json.Unmarshal(*gotHost.DeviceMapping, &dm))
	require.Len(t, dm, 3)

	// reporting google chrome profiles only clears chrome profiles from device mapping
	err = detailQueries["google_chrome_profiles"].DirectIngestFunc(
		context.Background(),
		kitlog.NewNopLogger(),
		&fleet.Host{ID: hostResp.Host.ID},
		s.ds,
		[]map[string]string{},
	)
	require.NoError(t, err)
	dmResp = listHostDeviceMappingResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/hosts/%d/device_mapping", hostResp.Host.ID), nil, http.StatusOK, &dmResp)
	require.Len(t, dmResp.DeviceMapping, 1)
	require.Equal(t, "sso_user@example.com", dmResp.DeviceMapping[0].Email)
	require.Equal(t, fleet.DeviceMappingMDMIdpAccounts, dmResp.DeviceMapping[0].Source)
	hostsResp = listHostsResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/hosts?query=%s&device_mapping=true", url.QueryEscape("sso_user@example.com")), nil, http.StatusOK, &hostsResp)
	require.Len(t, hostsResp.Hosts, 1)
	gotHost = hostsResp.Hosts[0]
	require.Equal(t, hostResp.Host.ID, gotHost.ID)
	require.NotNil(t, gotHost.DeviceMapping)
	dm = []fleet.HostDeviceMapping{}
	require.NoError(t, json.Unmarshal(*gotHost.DeviceMapping, &dm))
	require.Len(t, dm, 1)
	require.Equal(t, "sso_user@example.com", dm[0].Email)
	require.Equal(t, fleet.DeviceMappingMDMIdpAccounts, dm[0].Source)

	// enrolling a different user works without problems
	res = s.LoginMDMSSOUser("sso_user2", "user123#")
	require.NotEmpty(t, res.Header.Get("Location"))
	require.Equal(t, http.StatusSeeOther, res.StatusCode)
	u, err = url.Parse(res.Header.Get("Location"))
	require.NoError(t, err)
	q = u.Query()
	user2EnrollRef := q.Get("enrollment_reference")
	require.True(t, q.Has("eula_token"))
	require.True(t, q.Has("profile_token"))
	require.True(t, q.Has("enrollment_reference"))
	require.False(t, q.Has("error"))
	// the enrollment reference is different to the one used for the previous user
	require.NotEqual(t, user1EnrollRef, user2EnrollRef)
	// the url retrieves a valid profile
	s.downloadAndVerifyEnrollmentProfile(t, optsDownloadEnrollProf{
		basePath:  "/api/mdm/apple/enroll",
		enrollRef: user2EnrollRef,
		token:     q.Get("profile_token"),
		diParam:   di,
	})

	// the url retrieves a valid EULA
	resp = s.DoRaw("GET", "/api/latest/fleet/setup_experience/eula/"+q.Get("eula_token"), nil, http.StatusOK)
	require.EqualValues(t, len(pdfBytes), resp.ContentLength)
	require.Equal(t, "application/pdf", resp.Header.Get("content-type"))
	respBytes, err = io.ReadAll(resp.Body)
	require.NoError(t, err)
	require.EqualValues(t, pdfBytes, respBytes)

	// IdP info stored is accurate for the account
	s.checkStoredIdPInfo(t, user2EnrollRef, "sso_user2", "SSO User 2", "sso_user2@example.com")

	// changing the server URL also updates the remote DEP profile
	acResp = appConfigResponse{}
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
                "server_settings": {"server_url": "https://example.com"}
	}`), http.StatusOK, &acResp)

	s.runWorker()
	require.Contains(t, lastSubmittedProfile.URL, "https://example.com/mdm/sso")
	require.Equal(t, "https://example.com/mdm/sso", lastSubmittedProfile.ConfigurationWebURL)

	// Hitting the callback with an invalid SAMLResponse returns to the UI with an error.
	rawSSOResp := `InvalidXML`
	samlResponse := base64.StdEncoding.EncodeToString([]byte(rawSSOResp))
	res = s.DoRawNoAuth("POST", "/api/v1/fleet/mdm/sso/callback?SAMLResponse="+url.QueryEscape(samlResponse), nil, http.StatusSeeOther)
	require.NotEmpty(t, res.Header.Get("Location"))
	u, err = url.Parse(res.Header.Get("Location"))
	require.NoError(t, err)
	q = u.Query()
	require.False(t, q.Has("eula_token"))
	require.False(t, q.Has("profile_token"))
	require.False(t, q.Has("enrollment_reference"))
	require.True(t, q.Has("error"))

	// hitting the callback with an invalid session id redirects the user to the UI
	rawSSOResp = `<?xml version="1.0" encoding="UTF-8"?>
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Destination="https://localhost:8080/api/v1/fleet/sso/callback" ID="_7822b394622740aa92878ca6c7d1a28c53e80ec5ef" InResponseTo="4982b430-73e1-4ad2-885a-4a775a91f820" IssueInstant="2017-04-27T15:03:16.747Z" Version="2.0">
</samlp:Response>`
	samlResponse = base64.StdEncoding.EncodeToString([]byte(rawSSOResp))
	res = s.DoRawNoAuth("POST", "/api/v1/fleet/mdm/sso/callback?SAMLResponse="+url.QueryEscape(samlResponse), nil, http.StatusSeeOther)
	require.NotEmpty(t, res.Header.Get("Location"))
	u, err = url.Parse(res.Header.Get("Location"))
	require.NoError(t, err)
	q = u.Query()
	require.False(t, q.Has("eula_token"))
	require.False(t, q.Has("profile_token"))
	require.False(t, q.Has("enrollment_reference"))
	require.True(t, q.Has("error"))
}

func (s *integrationMDMTestSuite) checkStoredIdPInfo(t *testing.T, uuid, username, fullname, email string) {
	acc, err := s.ds.GetMDMIdPAccountByUUID(context.Background(), uuid)
	require.NoError(t, err)
	require.Equal(t, username, acc.Username)
	require.Equal(t, fullname, acc.Fullname)
	require.Equal(t, email, acc.Email)
}

func (s *integrationMDMTestSuite) TestSSOWithSCIM() {
	t := s.T()
	s.setSkipWorkerJobs(t)

	lastSubmittedProfile := &godep.Profile{}
	mdmDevice, _ := s.setUpEndUserAuthentication(t, lastSubmittedProfile)
	di, err := mdmtest.EncodeDeviceInfo(fleet.MDMAppleMachineInfo{
		Serial: mdmDevice.SerialNumber,
		UDID:   mdmDevice.UUID,
	})
	require.NoError(t, err)

	res := s.LoginMDMSSOUser("sso_user_no_displayname", "user123#")
	require.NotEmpty(t, res.Header.Get("Location"))
	require.Equal(t, http.StatusSeeOther, res.StatusCode)

	u, err := url.Parse(res.Header.Get("Location"))
	require.NoError(t, err)
	q := u.Query()
	user1EnrollRef := q.Get("enrollment_reference")
	// the url retrieves a valid profile
	prof := s.downloadAndVerifyEnrollmentProfile(t, optsDownloadEnrollProf{
		basePath:  "/api/mdm/apple/enroll",
		enrollRef: user1EnrollRef,
		token:     q.Get("profile_token"),
		diParam:   di,
	})

	// IdP info stored is accurate for the account
	s.checkStoredIdPInfo(t, user1EnrollRef, "sso_user_no_displayname", "", "sso_user_no_displayname@example.com")

	enrollURL := ""
	scepURL := ""
	for _, p := range prof.PayloadContent {
		switch p.PayloadType {
		case "com.apple.security.scep":
			scepURL = p.PayloadContent.URL
		case "com.apple.mdm":
			enrollURL = p.ServerURL
		}
	}
	require.NotEmpty(t, enrollURL)
	require.NotEmpty(t, scepURL)

	// enroll the device using the provided profile
	// we're using localhost for SSO because that's how the local
	// SimpleSAML server is configured, and s.server.URL changes between
	// test runs.
	mdmDevice.EnrollInfo.MDMURL = strings.Replace(enrollURL, "https://localhost:8080", s.server.URL, 1)
	mdmDevice.EnrollInfo.SCEPURL = strings.Replace(scepURL, "https://localhost:8080", s.server.URL, 1)
	err = mdmDevice.Enroll()
	require.NoError(t, err)

	// Add matching SCIM user with a display name (givenName + familyName)
	createUserPayload := map[string]interface{}{
		"schemas":    []string{"urn:ietf:params:scim:schemas:core:2.0:User"},
		"userName":   "some_other_username",
		"externalID": "external_id",
		"name": map[string]interface{}{
			"givenName":  "SCIM",
			"familyName": "User",
		},
		"emails": []map[string]interface{}{
			{
				"value":   "sso_user_no_displayname@example.com",
				"type":    "work",
				"primary": true,
			},
		},
	}
	displayName := "SCIM User"
	var createResp map[string]interface{}
	s.DoJSON("POST", "/api/latest/fleet/scim/Users", createUserPayload, http.StatusCreated, &createResp)
	// Verify the created user
	assert.Equal(t, "some_other_username", createResp["userName"])
	scimUserID, ok := createResp["id"]
	require.True(t, ok)

	// Enroll generated the TokenUpdate request to Fleet and enqueued the
	// Post-DEP enrollment job, it needs to be processed.
	s.runWorker()

	// ask for commands and verify that we get AccountConfiguration
	var accCmd *mdm.Command
	cmd, err := mdmDevice.Idle()
	require.NoError(t, err)
	for cmd != nil {
		if cmd.Command.RequestType == "AccountConfiguration" {
			accCmd = cmd
		}
		cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID)
		require.NoError(t, err)
	}
	require.NotNil(t, accCmd)
	require.NotNil(t, accCmd.Command)

	var fullAccCmd *micromdm.CommandPayload
	require.NoError(t, plist.Unmarshal(accCmd.Raw, &fullAccCmd))
	assert.True(t, fullAccCmd.Command.AccountConfiguration.LockPrimaryAccountInfo)
	assert.Equal(t, displayName, fullAccCmd.Command.AccountConfiguration.PrimaryAccountFullName)
	assert.Equal(t, "sso_user_no_displayname", fullAccCmd.Command.AccountConfiguration.PrimaryAccountUserName)

	// Report host details for the device. At this point, we should have fully linked the SCIM data with the user.
	var hostResp getHostResponse
	checkEndUser := func() {
		require.Len(t, hostResp.Host.EndUsers, 1)
		endUser := hostResp.Host.EndUsers[0]
		assert.Equal(t, "some_other_username", endUser.IdpUserName)
		assert.NotNil(t, endUser.IdpInfoUpdatedAt)
		assert.Equal(t, "external_id", endUser.IdpID)
		assert.Equal(t, displayName, endUser.IdpFullName)
		assert.Empty(t, endUser.IdpGroups)
		assert.Empty(t, endUser.OtherEmails)
	}
	s.DoJSON("GET", "/api/v1/fleet/hosts/identifier/"+mdmDevice.UUID, nil, http.StatusOK, &hostResp)
	assert.Len(t, hostResp.Host.EndUsers, 1)
	checkEndUser()

	hostID := hostResp.Host.ID
	hostResp = getHostResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/hosts/%d", hostID), nil, http.StatusOK, &hostResp)
	assert.Len(t, hostResp.Host.EndUsers, 1)
	checkEndUser()

	hostResp = getHostResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/hosts/%d", hostID), nil, http.StatusOK, &hostResp)
	checkEndUser()

	ac, err := s.ds.AppConfig(context.Background())
	require.NoError(t, err)

	detailQueries := osquery_utils.GetDetailQueries(context.Background(), config.FleetConfig{}, ac, &ac.Features, osquery_utils.Integrations{}, nil)

	// simulate osquery reporting mdm information, doesn't change anything
	rows := []map[string]string{
		{
			"enrolled":           "true",
			"installed_from_dep": "true",
			"server_url":         "https://test.example.com?enrollment_reference=" + user1EnrollRef,
			"payload_identifier": apple_mdm.FleetPayloadIdentifier,
		},
	}
	err = detailQueries["mdm"].DirectIngestFunc(
		context.Background(),
		kitlog.NewNopLogger(),
		&fleet.Host{ID: hostResp.Host.ID},
		s.ds,
		rows,
	)
	require.NoError(t, err)

	hostResp = getHostResponse{}
	s.DoJSON("GET", "/api/v1/fleet/hosts/identifier/"+mdmDevice.UUID, nil, http.StatusOK, &hostResp)
	checkEndUser()
	hostResp = getHostResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/hosts/%d", hostID), nil, http.StatusOK, &hostResp)
	checkEndUser()

	// sumulate osquery reporting chrome extension information
	rows = []map[string]string{
		{"email": "g1@example.com"},
		{"email": "g2@example.com"},
	}
	err = detailQueries["google_chrome_profiles"].DirectIngestFunc(
		context.Background(),
		kitlog.NewNopLogger(),
		&fleet.Host{ID: hostResp.Host.ID},
		s.ds,
		rows,
	)
	require.NoError(t, err)

	// mark any pending command as acknowledged at this point
	s.awaitTriggerProfileSchedule(t)
	cmd, err = mdmDevice.Idle()
	require.NoError(t, err)
	for cmd != nil {
		cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID)
		require.NoError(t, err)
	}

	// add two profiles to no team, one that uses the IdP username and one that uses groups
	s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
		{Name: "N1", Contents: mobileconfigForTest("N1", "I1", string(fleet.FleetVarHostEndUserIDPUsername))},
		{Name: "N2", Contents: mobileconfigForTest("N2", "I2", string(fleet.FleetVarHostEndUserIDPGroups))},
	}}, http.StatusNoContent)
	s.awaitTriggerProfileSchedule(t)

	// one will be failed (no IdP groups), and the other will succeed and be sent to the host
	// ask for commands and verify that we get InstallProfile for the username N1 profile
	var installProfCmd *mdm.Command
	cmd, err = mdmDevice.Idle()
	require.NoError(t, err)
	for cmd != nil {
		if cmd.Command.RequestType == "InstallProfile" {
			installProfCmd = cmd
		}
		cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID)
		require.NoError(t, err)
	}
	require.NotNil(t, installProfCmd)
	var decodedCmd struct {
		Command struct {
			Payload []byte
		}
	}
	err = plist.Unmarshal(installProfCmd.Raw, &decodedCmd)
	require.NoError(t, err)
	// variable was replaced with the IdP username
	require.Contains(t, string(decodedCmd.Command.Payload), "some_other_username")

	hostResp = getHostResponse{}
	s.DoJSON("GET", "/api/v1/fleet/hosts/identifier/"+mdmDevice.UUID, nil, http.StatusOK, &hostResp)
	require.NotNil(t, hostResp.Host.MDM.Profiles)
	var expectedCount int
	for _, prof := range *hostResp.Host.MDM.Profiles {
		switch prof.Name {
		case "N1": // profile with username
			expectedCount++
			require.NotNil(t, prof.Status)
			require.Equal(t, fleet.MDMDeliveryVerifying, *prof.Status)
		case "N2": // profile with group
			expectedCount++
			require.NotNil(t, prof.Status)
			require.Equal(t, fleet.MDMDeliveryFailed, *prof.Status)
		}
	}
	require.Equal(t, 2, expectedCount)

	// Also add a group to the SCIM user
	createGroup1Payload := map[string]interface{}{
		"schemas":     []string{"urn:ietf:params:scim:schemas:core:2.0:Group"},
		"displayName": "Test Group 1",
		"members": []map[string]interface{}{
			{
				"value": scimUserID,
			},
		},
	}
	var createGroup1Resp map[string]interface{}
	s.DoJSON("POST", "/api/latest/fleet/scim/Groups", createGroup1Payload, http.StatusCreated, &createGroup1Resp)

	// this marked profile N2 as pending for the host as the groups changed
	hostResp = getHostResponse{}
	s.DoJSON("GET", "/api/v1/fleet/hosts/identifier/"+mdmDevice.UUID, nil, http.StatusOK, &hostResp)
	require.NotNil(t, hostResp.Host.MDM.Profiles)
	expectedCount = 0
	for _, prof := range *hostResp.Host.MDM.Profiles {
		switch prof.Name {
		case "N1": // profile with username
			expectedCount++
			require.NotNil(t, prof.Status)
			require.Equal(t, fleet.MDMDeliveryVerifying, *prof.Status)
		case "N2": // profile with group
			expectedCount++
			require.NotNil(t, prof.Status)
			require.Equal(t, fleet.MDMDeliveryPending, *prof.Status)
		}
	}
	require.Equal(t, 2, expectedCount)

	// reconcile profiles
	s.awaitTriggerProfileSchedule(t)

	// ask for commands and verify that we get InstallProfile for the group N2 profile
	installProfCmd = nil
	cmd, err = mdmDevice.Idle()
	require.NoError(t, err)
	for cmd != nil {
		if cmd.Command.RequestType == "InstallProfile" {
			installProfCmd = cmd
		}
		cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID)
		require.NoError(t, err)
	}
	require.NotNil(t, installProfCmd)
	err = plist.Unmarshal(installProfCmd.Raw, &decodedCmd)
	require.NoError(t, err)
	// variable was replaced with the IdP group
	require.Contains(t, string(decodedCmd.Command.Payload), "Test Group 1")

	hostResp = getHostResponse{}
	s.DoJSON("GET", "/api/v1/fleet/hosts/identifier/"+mdmDevice.UUID, nil, http.StatusOK, &hostResp)

	// check that both profiles are now delivered
	require.NotNil(t, hostResp.Host.MDM.Profiles)
	expectedCount = 0
	for _, prof := range *hostResp.Host.MDM.Profiles {
		switch prof.Name {
		case "N1": // profile with username
			expectedCount++
			require.NotNil(t, prof.Status)
			require.Equal(t, fleet.MDMDeliveryVerifying, *prof.Status)
		case "N2": // profile with group
			expectedCount++
			require.NotNil(t, prof.Status)
			require.Equal(t, fleet.MDMDeliveryVerifying, *prof.Status)
		}
	}
	require.Equal(t, 2, expectedCount)

	checkEndUser = func() {
		require.Len(t, hostResp.Host.EndUsers, 1)
		endUser := hostResp.Host.EndUsers[0]
		assert.Equal(t, "some_other_username", endUser.IdpUserName) // This is a username, not an email.
		assert.NotNil(t, endUser.IdpInfoUpdatedAt)
		assert.Equal(t, "external_id", endUser.IdpID)
		assert.Equal(t, displayName, endUser.IdpFullName)
		require.EqualValues(t, []string{"Test Group 1"}, endUser.IdpGroups)
		require.Len(t, endUser.OtherEmails, 2)
		othersByEmail := make(map[string]string, 2)
		for _, otherEmail := range endUser.OtherEmails {
			othersByEmail[otherEmail.Email] = otherEmail.Source
		}
		source, ok := othersByEmail["g1@example.com"]
		require.True(t, ok)
		require.Equal(t, fleet.DeviceMappingGoogleChromeProfiles, source)
		source, ok = othersByEmail["g2@example.com"]
		require.True(t, ok)
		require.Equal(t, fleet.DeviceMappingGoogleChromeProfiles, source)
	}
	checkEndUser()
	hostResp = getHostResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/hosts/%d", hostID), nil, http.StatusOK, &hostResp)
	checkEndUser()

	// renew the mdm enrollment profile with a user that isn't in SCIM
	res = s.LoginMDMSSOUser("sso_user2", "user123#")
	require.NotEmpty(t, res.Header.Get("Location"))
	require.Equal(t, http.StatusSeeOther, res.StatusCode)
	u, err = url.Parse(res.Header.Get("Location"))
	require.NoError(t, err)
	q = u.Query()
	user2EnrollRef := q.Get("enrollment_reference")
	require.True(t, q.Has("profile_token"))
	require.True(t, q.Has("enrollment_reference"))
	require.False(t, q.Has("error"))
	// the enrollment reference is not same as the one used for the previous user
	require.NotEqual(t, user1EnrollRef, user2EnrollRef)
	// the url retrieves a valid profile
	prof = s.downloadAndVerifyEnrollmentProfile(t, optsDownloadEnrollProf{
		basePath:  "/api/mdm/apple/enroll",
		enrollRef: user2EnrollRef,
		token:     q.Get("profile_token"),
		diParam:   di,
	})

	// IdP info stored is accurate for the account
	s.checkStoredIdPInfo(t, user2EnrollRef, "sso_user2", "SSO User 2", "sso_user2@example.com")

	enrollURL = ""
	scepURL = ""
	for _, p := range prof.PayloadContent {
		switch p.PayloadType {
		case "com.apple.security.scep":
			scepURL = p.PayloadContent.URL
		case "com.apple.mdm":
			enrollURL = p.ServerURL
		}
	}
	require.NotEmpty(t, enrollURL)
	require.NotEmpty(t, scepURL)

	// enroll the device using the provided profile
	mdmDevice.EnrollInfo.MDMURL = strings.Replace(enrollURL, "https://localhost:8080", s.server.URL, 1)
	mdmDevice.EnrollInfo.SCEPURL = strings.Replace(scepURL, "https://localhost:8080", s.server.URL, 1)
	err = mdmDevice.Enroll()
	require.NoError(t, err)

	hostResp = getHostResponse{}
	s.DoJSON("GET", "/api/v1/fleet/hosts/identifier/"+mdmDevice.UUID, nil, http.StatusOK, &hostResp)
	checkEndUser = func() {
		require.Len(t, hostResp.Host.EndUsers, 1)
		endUser := hostResp.Host.EndUsers[0]
		assert.Equal(t, "sso_user2@example.com", endUser.IdpUserName)
		assert.Empty(t, endUser.IdpInfoUpdatedAt)
		assert.Empty(t, endUser.IdpID)
		assert.Empty(t, endUser.IdpFullName)
		assert.Empty(t, endUser.IdpGroups)
		assert.Len(t, endUser.OtherEmails, 2) // prior google chrome profiles are still associated
	}
	checkEndUser()
	hostResp = getHostResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/hosts/%d", hostID), nil, http.StatusOK, &hostResp)
	checkEndUser()

	// TODO: add tests for other scenarios:
	// - re-enroll via SSO flow with a different SCIM user (IdP user should be updated to include all SCIM info)
	// - re-enroll via non-SSO flow (IdP user should be entirely empty)
	// - multiple SCIM integrations with overlapping emails (how would that even work?)
	// - SSO flow configured to use IdP different from SCIM but with overlapping emails (how would that even work?)
}

// Note if you call this function it will change your server URL which may cause certain downstream
// things in your test to fail. Either change the SSO Server URL back after doing SSO or make changes
// in your test to account for the hardcoded, wrong server URL of https://localhost:8080 being used
// in places like enrollment profiles
func (s *integrationMDMTestSuite) setUpMDMSSO(t *testing.T) appConfigResponse {
	// MDM SSO fields are empty by default
	acResp := appConfigResponse{}
	s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp)
	assert.Empty(t, acResp.MDM.EndUserAuthentication.SSOProviderSettings)

	// set the SSO fields
	acResp = appConfigResponse{}
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
			"server_settings": {
				"server_url": "https://localhost:8080"
			},
			"mdm": {
				"end_user_authentication": {
					"entity_id": "mdm.test.com",
					"idp_name": "SimpleSAML",
					"metadata_url": "http://localhost:9080/simplesaml/saml2/idp/metadata.php"
				},
				"macos_setup": {
					"enable_end_user_authentication": true
				}
			}
		}`), http.StatusOK, &acResp)
	wantSettings := fleet.SSOProviderSettings{
		EntityID:    "mdm.test.com",
		IDPName:     "SimpleSAML",
		MetadataURL: "http://localhost:9080/simplesaml/saml2/idp/metadata.php",
	}
	assert.Equal(t, wantSettings, acResp.MDM.EndUserAuthentication.SSOProviderSettings)

	t.Cleanup(func() {
		s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
				"server_settings": {
					"server_url": "`+s.server.URL+`"
				},
				"mdm": {
					"end_user_authentication": {
						"entity_id": "",
						"idp_name": "",
						"metadata_url": ""
					},
					"macos_setup": {
						"enable_end_user_authentication": false
					}
				}
			}`), http.StatusOK, &acResp)
	})

	// check that they are returned by a GET /config
	acResp = appConfigResponse{}
	s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp)
	assert.Equal(t, wantSettings, acResp.MDM.EndUserAuthentication.SSOProviderSettings)
	return acResp
}

// If you call this function see note on setUpMDMSSO() about server URL changes
func (s *integrationMDMTestSuite) setUpEndUserAuthentication(t *testing.T, lastSubmittedProfile *godep.Profile) (*mdmtest.TestAppleMDMClient,
	fleet.SSOProviderSettings,
) {
	mdmDevice := mdmtest.NewTestMDMClientAppleDirect(mdmtest.AppleEnrollInfo{
		SCEPChallenge: s.scepChallenge,
	}, "MacBookPro16,1")
	s.enableABM(t.Name())
	s.mockDEPResponse(t.Name(), http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
		switch r.URL.Path {
		case "/session":
			_, _ = w.Write([]byte(`{"auth_session_token": "xyz"}`))
		case "/profile":
			*lastSubmittedProfile = godep.Profile{}
			rawProfile, err := io.ReadAll(r.Body)
			require.NoError(t, err)
			err = json.Unmarshal(rawProfile, lastSubmittedProfile)
			require.NoError(t, err)
			encoder := json.NewEncoder(w)
			err = encoder.Encode(godep.ProfileResponse{ProfileUUID: "abc"})
			require.NoError(t, err)
		case "/profile/devices":
			b, err := io.ReadAll(r.Body)
			require.NoError(t, err)
			var prof profileAssignmentReq
			require.NoError(t, json.Unmarshal(b, &prof))
			var resp godep.ProfileResponse
			resp.ProfileUUID = prof.ProfileUUID
			resp.Devices = map[string]string{
				prof.Devices[0]: string(fleet.DEPAssignProfileResponseSuccess),
			}
			encoder := json.NewEncoder(w)
			err = encoder.Encode(resp)
			require.NoError(t, err)
		case "/server/devices", "/devices/sync":
			// This endpoint  is used to get an initial list of
			// devices, return a single device
			encoder := json.NewEncoder(w)
			err := encoder.Encode(godep.DeviceResponse{
				Devices: []godep.Device{
					{
						SerialNumber: mdmDevice.SerialNumber,
						Model:        mdmDevice.Model,
						OS:           "osx",
						OpType:       "added",
					},
				},
			})
			require.NoError(t, err)
		}
	}))

	// sync the list of ABM devices
	s.runDEPSchedule()

	acResp := s.setUpMDMSSO(t)

	// trigger the worker to process the job and wait for result before continuing.
	s.runWorker()

	// check that the last submitted DEP profile has been updated accordingly
	require.Contains(t, lastSubmittedProfile.URL, acResp.ServerSettings.ServerURL+"/mdm/sso")
	require.Equal(t, acResp.ServerSettings.ServerURL+"/mdm/sso", lastSubmittedProfile.ConfigurationWebURL)
	return mdmDevice, acResp.MDM.EndUserAuthentication.SSOProviderSettings
}

type scepPayload struct {
	Challenge string
	URL       string
}

type enrollmentPayload struct {
	PayloadType            string
	ServerURL              string      // used by the enrollment payload
	PayloadContent         scepPayload // scep contains a nested payload content dict
	AssignedManagedAppleID string      // used by account driven enrollment. Not always present
}

type enrollmentProfile struct {
	PayloadIdentifier string
	PayloadContent    []enrollmentPayload
}

type optsDownloadEnrollProf struct {
	basePath  string
	token     string
	enrollRef string
	diParam   string
	diHeader  string
}

func (s *integrationMDMTestSuite) downloadAndVerifyEnrollmentProfile(t *testing.T, opts optsDownloadEnrollProf) *enrollmentProfile {
	var headers map[string]string
	if opts.diHeader != "" {
		headers = map[string]string{"X-apple-aspen-deviceinfo": opts.diHeader}
	}

	path := opts.basePath
	if path == "" {
		path = "/api/mdm/apple/enroll"
	}

	var queryParams []string
	if opts.token != "" {
		queryParams = append(queryParams, "token", opts.token)
	}
	if opts.enrollRef != "" {
		queryParams = append(queryParams, "enrollment_reference", opts.enrollRef)
	}
	if opts.diParam != "" {
		queryParams = append(queryParams, "deviceinfo", opts.diParam)
	}

	resp := s.DoRawWithHeaders("GET", path, nil, http.StatusOK, headers, queryParams...)
	defer resp.Body.Close()
	body, err := io.ReadAll(resp.Body)
	require.NoError(t, err)
	require.Contains(t, resp.Header, "Content-Disposition")
	require.Contains(t, resp.Header, "Content-Type")
	require.Contains(t, resp.Header, "X-Content-Type-Options")
	require.Contains(t, resp.Header.Get("Content-Disposition"), "attachment;")
	require.Contains(t, resp.Header.Get("Content-Type"), "application/x-apple-aspen-config")
	require.Contains(t, resp.Header.Get("X-Content-Type-Options"), "nosniff")
	headerLen, err := strconv.Atoi(resp.Header.Get("Content-Length"))
	require.NoError(t, err)
	require.Equal(t, len(body), headerLen)

	return s.verifyEnrollmentProfile(body, "", "")
}

func (s *integrationMDMTestSuite) downloadAndVerifyOTAEnrollmentProfile(path string) {
	t := s.T()

	resp := s.DoRaw("GET", path, nil, http.StatusOK)
	rawProfile, err := io.ReadAll(resp.Body)
	resp.Body.Close()
	require.NoError(t, err)
	require.Contains(t, resp.Header, "Content-Disposition")
	require.Contains(t, resp.Header, "Content-Type")
	require.Contains(t, resp.Header, "X-Content-Type-Options")
	require.Contains(t, resp.Header.Get("Content-Disposition"), "attachment;")
	require.Contains(t, resp.Header.Get("Content-Type"), "application/x-apple-aspen-config")
	require.Contains(t, resp.Header.Get("X-Content-Type-Options"), "nosniff")
	headerLen, err := strconv.Atoi(resp.Header.Get("Content-Length"))
	require.NoError(t, err)
	require.Equal(t, len(rawProfile), headerLen)

	p7, err := pkcs7.Parse(rawProfile)
	require.NoError(t, err)
	rootCA := x509.NewCertPool()

	assets, err := s.ds.GetAllMDMConfigAssetsByName(context.Background(), []fleet.MDMAssetName{
		fleet.MDMAssetCACert,
	}, nil)
	require.NoError(t, err)

	require.True(t, rootCA.AppendCertsFromPEM(assets[fleet.MDMAssetCACert].Value))
	require.NoError(t, p7.VerifyWithChain(rootCA))

	var otaEnrollmentProfile struct {
		PayloadContent struct {
			URL string `plist:"URL"`
		} `plist:"PayloadContent"`
	}
	err = plist.Unmarshal(p7.Content, &otaEnrollmentProfile)
	require.NoError(t, err)
	require.Contains(t, otaEnrollmentProfile.PayloadContent.URL, s.getConfig().ServerSettings.ServerURL+"/api/v1/fleet/ota_enrollment")
}

func (s *integrationMDMTestSuite) downloadAndVerifyEnrollmentProfileManual(t *testing.T) {
	resp := s.DoRaw("GET", "/api/latest/fleet/enrollment_profiles/manual", nil, http.StatusOK)
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	require.NoError(t, err)
	require.Contains(t, resp.Header, "Content-Disposition")
	require.Contains(t, resp.Header, "Content-Type")
	require.Contains(t, resp.Header, "X-Content-Type-Options")
	require.Contains(t, resp.Header.Get("Content-Disposition"), "attachment;")
	require.Contains(t, resp.Header.Get("Content-Type"), "application/x-apple-aspen-config")
	require.Contains(t, resp.Header.Get("X-Content-Type-Options"), "nosniff")
	headerLen, err := strconv.Atoi(resp.Header.Get("Content-Length"))
	require.NoError(t, err)
	require.Equal(t, len(body), headerLen)

	s.verifyEnrollmentProfile(body, "", "")
}

func (s *integrationMDMTestSuite) verifyEnrollmentProfile(rawProfile []byte, enrollmentRef, managedAppleAccount string) *enrollmentProfile {
	t := s.T()
	var profile enrollmentProfile

	if !bytes.HasPrefix(bytes.TrimSpace(rawProfile), []byte("<?xml")) {
		p7, err := pkcs7.Parse(rawProfile)
		require.NoError(t, err)
		rootCA := x509.NewCertPool()

		assets, err := s.ds.GetAllMDMConfigAssetsByName(context.Background(), []fleet.MDMAssetName{
			fleet.MDMAssetCACert,
		}, nil)
		require.NoError(t, err)

		require.True(t, rootCA.AppendCertsFromPEM(assets[fleet.MDMAssetCACert].Value))
		require.NoError(t, p7.VerifyWithChain(rootCA))
		rawProfile = p7.Content
	}

	require.NoError(t, plist.Unmarshal(rawProfile, &profile))

	for _, p := range profile.PayloadContent {
		switch p.PayloadType {
		case "com.apple.security.scep":
			require.Equal(t, s.getConfig().ServerSettings.ServerURL+apple_mdm.SCEPPath, p.PayloadContent.URL)
			require.Equal(t, s.scepChallenge, p.PayloadContent.Challenge)
		case "com.apple.mdm":
			require.Contains(t, p.ServerURL, s.getConfig().ServerSettings.ServerURL+apple_mdm.MDMPath)
			if enrollmentRef != "" {
				require.Contains(t, p.ServerURL, enrollmentRef)
			}
			if managedAppleAccount != "" {
				require.Equal(t, p.AssignedManagedAppleID, managedAppleAccount)
			}
		default:
			require.Failf(t, "unrecognized payload type in enrollment profile: %s", p.PayloadType)
		}
	}
	return &profile
}

func (s *integrationMDMTestSuite) TestMDMMigration() {
	t := s.T()
	ctx := context.Background()

	// enable migration
	var acResp appConfigResponse
	s.DoJSON("PATCH", "/api/v1/fleet/config", json.RawMessage(`{
		"mdm": { "macos_migration": { "enable": true, "mode": "voluntary", "webhook_url": "https://example.com" } }
	}`), http.StatusOK, &acResp)

	abmToken := s.enableABM(t.Name())

	checkMigrationResponses := func(host *fleet.Host, token string) {
		getDesktopResp := fleetDesktopResponse{}
		res := s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token+"/desktop", nil, http.StatusOK)
		require.NoError(t, json.NewDecoder(res.Body).Decode(&getDesktopResp))
		require.NoError(t, res.Body.Close())
		require.NoError(t, getDesktopResp.Err)
		require.Zero(t, *getDesktopResp.FailingPolicies)
		require.False(t, getDesktopResp.Notifications.NeedsMDMMigration)
		require.False(t, getDesktopResp.Notifications.RenewEnrollmentProfile)
		require.Equal(t, acResp.OrgInfo.OrgLogoURL, getDesktopResp.Config.OrgInfo.OrgLogoURL)
		require.Equal(t, acResp.OrgInfo.OrgLogoURLLightBackground, getDesktopResp.Config.OrgInfo.OrgLogoURLLightBackground)
		require.Equal(t, acResp.OrgInfo.ContactURL, getDesktopResp.Config.OrgInfo.ContactURL)
		require.Equal(t, acResp.OrgInfo.OrgName, getDesktopResp.Config.OrgInfo.OrgName)
		require.Equal(t, acResp.MDM.MacOSMigration.Mode, getDesktopResp.Config.MDM.MacOSMigration.Mode)

		orbitConfigResp := orbitGetConfigResponse{}
		s.DoJSON("POST", "/api/fleet/orbit/config", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *host.OrbitNodeKey)), http.StatusOK, &orbitConfigResp)
		require.False(t, orbitConfigResp.Notifications.NeedsMDMMigration)
		require.False(t, orbitConfigResp.Notifications.RenewEnrollmentProfile)

		// simulate that the device is assigned to Fleet in ABM
		profileAssignmentStatusResponse := fleet.DEPAssignProfileResponseSuccess
		s.mockDEPResponse(t.Name(), http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			w.WriteHeader(http.StatusOK)
			switch r.URL.Path {
			case "/session":
				_, _ = w.Write([]byte(`{"auth_session_token": "xyz"}`))
			case "/profile":
				encoder := json.NewEncoder(w)
				err := encoder.Encode(godep.ProfileResponse{ProfileUUID: "abc"})
				require.NoError(t, err)
			case "/server/devices", "/devices/sync":
				encoder := json.NewEncoder(w)
				err := encoder.Encode(godep.DeviceResponse{
					Devices: []godep.Device{
						{
							SerialNumber: host.HardwareSerial,
							Model:        "Mac Mini",
							OS:           "osx",
							OpType:       "added",
						},
					},
				})
				require.NoError(t, err)
			case "/profile/devices":
				b, err := io.ReadAll(r.Body)
				require.NoError(t, err)
				var prof profileAssignmentReq
				require.NoError(t, json.Unmarshal(b, &prof))
				var resp godep.ProfileResponse
				resp.ProfileUUID = prof.ProfileUUID
				resp.Devices = map[string]string{
					prof.Devices[0]: string(profileAssignmentStatusResponse),
				}
				encoder := json.NewEncoder(w)
				err = encoder.Encode(resp)
				require.NoError(t, err)
			}
		}))

		cleanAssignmentStatus := func() {
			mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
				stmt := `UPDATE host_dep_assignments
					 SET assign_profile_response = NULL,
					     response_updated_at = NULL,
					     profile_uuid = NULL
					 WHERE host_id = ?`
				_, err := q.ExecContext(context.Background(), stmt, host.ID)
				return err
			})
		}

		// simulate that the device is enrolled in a third-party MDM and DEP capable
		err := s.ds.SetOrUpdateMDMData(
			ctx,
			host.ID,
			false,
			true,
			"https://simplemdm.com",
			true,
			fleet.WellKnownMDMSimpleMDM,
			"",
			false,
		)
		require.NoError(t, err)

		// simulate a response before we have the chance to assign the profile
		getDesktopResp = fleetDesktopResponse{}
		res = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token+"/desktop", nil, http.StatusOK)
		require.NoError(t, json.NewDecoder(res.Body).Decode(&getDesktopResp))
		require.NoError(t, res.Body.Close())
		require.NoError(t, getDesktopResp.Err)
		require.Zero(t, *getDesktopResp.FailingPolicies)
		require.False(t, getDesktopResp.Notifications.NeedsMDMMigration)
		require.False(t, getDesktopResp.Notifications.RenewEnrollmentProfile)
		require.Equal(t, acResp.OrgInfo.OrgLogoURL, getDesktopResp.Config.OrgInfo.OrgLogoURL)
		require.Equal(t, acResp.OrgInfo.OrgLogoURLLightBackground, getDesktopResp.Config.OrgInfo.OrgLogoURLLightBackground)
		require.Equal(t, acResp.OrgInfo.ContactURL, getDesktopResp.Config.OrgInfo.ContactURL)
		require.Equal(t, acResp.OrgInfo.OrgName, getDesktopResp.Config.OrgInfo.OrgName)
		require.Equal(t, acResp.MDM.MacOSMigration.Mode, getDesktopResp.Config.MDM.MacOSMigration.Mode)
		orbitConfigResp = orbitGetConfigResponse{}
		s.DoJSON("POST", "/api/fleet/orbit/config", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *host.OrbitNodeKey)), http.StatusOK, &orbitConfigResp)
		require.False(t, orbitConfigResp.Notifications.NeedsMDMMigration)
		require.False(t, orbitConfigResp.Notifications.RenewEnrollmentProfile)
		cleanAssignmentStatus()

		// simulate a "FAILED" JSON profile assignment
		profileAssignmentStatusResponse = fleet.DEPAssignProfileResponseFailed
		s.runDEPSchedule()
		getDesktopResp = fleetDesktopResponse{}
		res = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token+"/desktop", nil, http.StatusOK)
		require.NoError(t, json.NewDecoder(res.Body).Decode(&getDesktopResp))
		require.NoError(t, res.Body.Close())
		require.False(t, getDesktopResp.Notifications.NeedsMDMMigration)
		require.False(t, orbitConfigResp.Notifications.RenewEnrollmentProfile)
		orbitConfigResp = orbitGetConfigResponse{}
		s.DoJSON("POST", "/api/fleet/orbit/config", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *host.OrbitNodeKey)), http.StatusOK, &orbitConfigResp)
		require.False(t, orbitConfigResp.Notifications.NeedsMDMMigration)
		require.False(t, orbitConfigResp.Notifications.RenewEnrollmentProfile)
		require.NoError(t, s.ds.DeleteHostDEPAssignments(ctx, abmToken.ID, []string{host.HardwareSerial}))
		cleanAssignmentStatus()

		// simulate a "NOT_ACCESSIBLE" JSON profile assignment
		profileAssignmentStatusResponse = fleet.DEPAssignProfileResponseNotAccessible
		s.runDEPSchedule()
		getDesktopResp = fleetDesktopResponse{}
		res = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token+"/desktop", nil, http.StatusOK)
		require.NoError(t, json.NewDecoder(res.Body).Decode(&getDesktopResp))
		require.NoError(t, res.Body.Close())
		require.False(t, getDesktopResp.Notifications.NeedsMDMMigration)
		require.False(t, orbitConfigResp.Notifications.RenewEnrollmentProfile)
		s.DoJSON("POST", "/api/fleet/orbit/config", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *host.OrbitNodeKey)), http.StatusOK, &orbitConfigResp)
		require.False(t, orbitConfigResp.Notifications.NeedsMDMMigration)
		require.False(t, orbitConfigResp.Notifications.RenewEnrollmentProfile)
		require.NoError(t, s.ds.DeleteHostDEPAssignments(ctx, abmToken.ID, []string{host.HardwareSerial}))
		cleanAssignmentStatus()

		// simulate a "SUCCESS" JSON profile assignment
		profileAssignmentStatusResponse = fleet.DEPAssignProfileResponseSuccess
		s.runDEPSchedule()
		getDesktopResp = fleetDesktopResponse{}
		res = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token+"/desktop", nil, http.StatusOK)
		require.NoError(t, json.NewDecoder(res.Body).Decode(&getDesktopResp))
		require.NoError(t, res.Body.Close())
		require.True(t, getDesktopResp.Notifications.NeedsMDMMigration)
		require.False(t, orbitConfigResp.Notifications.RenewEnrollmentProfile)
		s.DoJSON("POST", "/api/fleet/orbit/config", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *host.OrbitNodeKey)), http.StatusOK, &orbitConfigResp)
		require.True(t, orbitConfigResp.Notifications.NeedsMDMMigration)
		require.False(t, orbitConfigResp.Notifications.RenewEnrollmentProfile)

		// simulate that the device needs to be enrolled in fleet, DEP capable
		err = s.ds.SetOrUpdateMDMData(
			ctx,
			host.ID,
			false,
			false,
			s.server.URL,
			true,
			fleet.WellKnownMDMFleet,
			"",
			false,
		)
		require.NoError(t, err)

		getDesktopResp = fleetDesktopResponse{}
		res = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token+"/desktop", nil, http.StatusOK)
		require.NoError(t, json.NewDecoder(res.Body).Decode(&getDesktopResp))
		require.NoError(t, res.Body.Close())
		require.NoError(t, getDesktopResp.Err)
		require.Zero(t, *getDesktopResp.FailingPolicies)
		require.False(t, getDesktopResp.Notifications.NeedsMDMMigration)
		require.True(t, getDesktopResp.Notifications.RenewEnrollmentProfile)
		require.Equal(t, acResp.OrgInfo.OrgLogoURL, getDesktopResp.Config.OrgInfo.OrgLogoURL)
		require.Equal(t, acResp.OrgInfo.OrgLogoURLLightBackground, getDesktopResp.Config.OrgInfo.OrgLogoURLLightBackground)
		require.Equal(t, acResp.OrgInfo.ContactURL, getDesktopResp.Config.OrgInfo.ContactURL)
		require.Equal(t, acResp.OrgInfo.OrgName, getDesktopResp.Config.OrgInfo.OrgName)
		require.Equal(t, acResp.MDM.MacOSMigration.Mode, getDesktopResp.Config.MDM.MacOSMigration.Mode)

		orbitConfigResp = orbitGetConfigResponse{}
		s.DoJSON("POST", "/api/fleet/orbit/config", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *host.OrbitNodeKey)), http.StatusOK, &orbitConfigResp)
		require.False(t, orbitConfigResp.Notifications.NeedsMDMMigration)
		require.True(t, orbitConfigResp.Notifications.RenewEnrollmentProfile)

		// simulate that the device is manually enrolled into fleet, but DEP capable
		err = s.ds.SetOrUpdateMDMData(
			ctx,
			host.ID,
			false,
			true,
			s.server.URL,
			false,
			fleet.WellKnownMDMFleet,
			"",
			false,
		)
		require.NoError(t, err)
		mdmDevice := mdmtest.NewTestMDMClientAppleDirect(mdmtest.AppleEnrollInfo{
			SCEPChallenge: s.scepChallenge,
			SCEPURL:       s.server.URL + apple_mdm.SCEPPath,
			MDMURL:        s.server.URL + apple_mdm.MDMPath,
		}, "MacBookPro16,1")
		mdmDevice.SerialNumber = host.HardwareSerial
		mdmDevice.UUID = host.UUID
		err = mdmDevice.Enroll()
		require.NoError(t, err)
		require.NoError(t, err)
		getDesktopResp = fleetDesktopResponse{}
		res = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token+"/desktop", nil, http.StatusOK)
		require.NoError(t, json.NewDecoder(res.Body).Decode(&getDesktopResp))
		require.NoError(t, res.Body.Close())
		require.NoError(t, getDesktopResp.Err)
		require.Zero(t, *getDesktopResp.FailingPolicies)
		require.False(t, getDesktopResp.Notifications.NeedsMDMMigration)
		require.False(t, getDesktopResp.Notifications.RenewEnrollmentProfile)
		require.Equal(t, acResp.OrgInfo.OrgLogoURL, getDesktopResp.Config.OrgInfo.OrgLogoURL)
		require.Equal(t, acResp.OrgInfo.OrgLogoURLLightBackground, getDesktopResp.Config.OrgInfo.OrgLogoURLLightBackground)
		require.Equal(t, acResp.OrgInfo.ContactURL, getDesktopResp.Config.OrgInfo.ContactURL)
		require.Equal(t, acResp.OrgInfo.OrgName, getDesktopResp.Config.OrgInfo.OrgName)
		require.Equal(t, acResp.MDM.MacOSMigration.Mode, getDesktopResp.Config.MDM.MacOSMigration.Mode)

		orbitConfigResp = orbitGetConfigResponse{}
		s.DoJSON("POST", "/api/fleet/orbit/config", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *host.OrbitNodeKey)), http.StatusOK, &orbitConfigResp)
		require.False(t, orbitConfigResp.Notifications.NeedsMDMMigration)
		require.False(t, orbitConfigResp.Notifications.RenewEnrollmentProfile)

		// simulate a host that was reset to factory, but fleet still has an mdm record active
		err = s.ds.SetOrUpdateMDMData(
			ctx,
			host.ID,
			false,
			true,
			s.server.URL,
			false,
			fleet.WellKnownMDMSimpleMDM,
			"",
			false,
		)
		require.NoError(t, err)
		getDesktopResp = fleetDesktopResponse{}
		res = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token+"/desktop", nil, http.StatusOK)
		require.NoError(t, json.NewDecoder(res.Body).Decode(&getDesktopResp))
		require.NoError(t, res.Body.Close())
		require.NoError(t, getDesktopResp.Err)
		require.Zero(t, *getDesktopResp.FailingPolicies)
		require.True(t, getDesktopResp.Notifications.NeedsMDMMigration)
		require.False(t, getDesktopResp.Notifications.RenewEnrollmentProfile)
		require.Equal(t, acResp.OrgInfo.OrgLogoURL, getDesktopResp.Config.OrgInfo.OrgLogoURL)
		require.Equal(t, acResp.OrgInfo.OrgLogoURLLightBackground, getDesktopResp.Config.OrgInfo.OrgLogoURLLightBackground)
		require.Equal(t, acResp.OrgInfo.ContactURL, getDesktopResp.Config.OrgInfo.ContactURL)
		require.Equal(t, acResp.OrgInfo.OrgName, getDesktopResp.Config.OrgInfo.OrgName)
		require.Equal(t, acResp.MDM.MacOSMigration.Mode, getDesktopResp.Config.MDM.MacOSMigration.Mode)

		orbitConfigResp = orbitGetConfigResponse{}
		s.DoJSON("POST", "/api/fleet/orbit/config", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *host.OrbitNodeKey)), http.StatusOK, &orbitConfigResp)
		require.True(t, orbitConfigResp.Notifications.NeedsMDMMigration)
		require.False(t, orbitConfigResp.Notifications.RenewEnrollmentProfile)

		// simulate a device that is manually enrolled to 3rd party
		err = s.ds.SetOrUpdateMDMData(
			ctx,
			host.ID,
			false,
			true,
			"https://simplemdm.com",
			false,
			fleet.WellKnownMDMSimpleMDM,
			"",
			false,
		)
		require.NoError(t, err)
		getDesktopResp = fleetDesktopResponse{}
		res = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token+"/desktop", nil, http.StatusOK)
		require.NoError(t, json.NewDecoder(res.Body).Decode(&getDesktopResp))
		require.NoError(t, res.Body.Close())
		require.NoError(t, getDesktopResp.Err)
		require.Zero(t, *getDesktopResp.FailingPolicies)
		require.True(t, getDesktopResp.Notifications.NeedsMDMMigration)
		require.False(t, getDesktopResp.Notifications.RenewEnrollmentProfile)
		require.Equal(t, acResp.OrgInfo.OrgLogoURL, getDesktopResp.Config.OrgInfo.OrgLogoURL)
		require.Equal(t, acResp.OrgInfo.OrgLogoURLLightBackground, getDesktopResp.Config.OrgInfo.OrgLogoURLLightBackground)
		require.Equal(t, acResp.OrgInfo.ContactURL, getDesktopResp.Config.OrgInfo.ContactURL)
		require.Equal(t, acResp.OrgInfo.OrgName, getDesktopResp.Config.OrgInfo.OrgName)
		require.Equal(t, acResp.MDM.MacOSMigration.Mode, getDesktopResp.Config.MDM.MacOSMigration.Mode)

		orbitConfigResp = orbitGetConfigResponse{}
		s.DoJSON("POST", "/api/fleet/orbit/config", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *host.OrbitNodeKey)), http.StatusOK, &orbitConfigResp)
		require.True(t, orbitConfigResp.Notifications.NeedsMDMMigration)
		require.False(t, orbitConfigResp.Notifications.RenewEnrollmentProfile)

		// clean up nano tables
		mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
			_, err := q.ExecContext(context.Background(), `
			DELETE FROM nano_enrollments WHERE id = ?
			`, host.UUID)
			return err
		})
		mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
			_, err := q.ExecContext(context.Background(), `
			DELETE FROM nano_devices WHERE id = ?
			`, host.UUID)
			return err
		})
	}

	token := "token_test_migration"
	host := createOrbitEnrolledHost(t, "darwin", "h", s.ds)
	createDeviceTokenForHost(t, s.ds, host.ID, token)
	checkMigrationResponses(host, token)
	require.NoError(t, s.ds.DeleteHostDEPAssignments(ctx, abmToken.ID, []string{host.HardwareSerial}))

	tm, err := s.ds.NewTeam(ctx, &fleet.Team{Name: "team-1"})
	require.NoError(t, err)
	err = s.ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&tm.ID, []uint{host.ID}))
	require.NoError(t, err)
	checkMigrationResponses(host, token)
}

// ///////////////////////////////////////////////////////////////////////////
// Windows MDM tests

func (s *integrationMDMTestSuite) TestAppConfigWindowsMDM() {
	ctx := context.Background()
	t := s.T()

	appConf, err := s.ds.AppConfig(context.Background())
	require.NoError(s.T(), err)
	appConf.MDM.WindowsEnabledAndConfigured = false
	err = s.ds.SaveAppConfig(context.Background(), appConf)
	require.NoError(s.T(), err)

	var acResp appConfigResponse
	s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp)
	assert.False(t, acResp.MDM.WindowsEnabledAndConfigured)

	// create a couple teams
	tm1, err := s.ds.NewTeam(ctx, &fleet.Team{Name: t.Name() + "1"})
	require.NoError(t, err)
	tm2, err := s.ds.NewTeam(ctx, &fleet.Team{Name: t.Name() + "2"})
	require.NoError(t, err)

	// enable Windows MDM
	acResp = appConfigResponse{}
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
		"mdm": { "windows_enabled_and_configured": true }
  }`), http.StatusOK, &acResp)
	assert.True(t, acResp.MDM.WindowsEnabledAndConfigured)
	assert.False(t, acResp.MDM.WindowsMigrationEnabled)
	s.lastActivityOfTypeMatches(fleet.ActivityTypeEnabledWindowsMDM{}.ActivityName(), `{}`, 0)

	// create some hosts - a Windows workstation in each team and no-team,
	// Windows server in no team, Windows workstation enrolled in a 3rd-party in
	// team 2, Windows workstation already enrolled in Fleet in no team, and a
	// macOS host in no team.
	metadataHosts := []struct {
		os            string
		suffix        string
		isServer      bool
		teamID        *uint
		enrolledName  string
		shouldEnroll  bool
		shouldMigrate bool
	}{
		{"windows", "win-no-team", false, nil, "", true, false},
		{"windows", "win-team-1", false, &tm1.ID, "", true, false},
		{"windows", "win-team-2", false, &tm2.ID, "", true, false},
		{"windows", "win-server", true, nil, "", false, false},                                      // is a server
		{"windows", "win-third-party", false, &tm2.ID, fleet.WellKnownMDMSimpleMDM, false, true},    // is enrolled in 3rd-party
		{"windows", "win-fleet", false, nil, fleet.WellKnownMDMFleet, false, false},                 // is already Fleet-enrolled
		{"darwin", "macos-no-team", false, nil, "", false, false},                                   // is not Windows
		{"windows", "win-server-third-party", true, nil, fleet.WellKnownMDMSimpleMDM, false, false}, // is enrolled in 3rd-party, but is a server
	}
	hostsBySuffix := make(map[string]*fleet.Host, len(metadataHosts))
	for _, meta := range metadataHosts {
		var host *fleet.Host
		if meta.os == "windows" && meta.enrolledName == fleet.WellKnownMDMFleet {
			// special-case to create a properly MDM-enrolled into Fleet host
			host = createOrbitEnrolledHost(t, meta.os, meta.suffix, s.ds)
			mdmDevice := mdmtest.NewTestMDMClientWindowsProgramatic(s.server.URL, *host.OrbitNodeKey)
			err := mdmDevice.Enroll()
			require.NoError(t, err)
			updated, err := s.ds.UpdateMDMWindowsEnrollmentsHostUUID(ctx, host.UUID, mdmDevice.DeviceID)
			require.NoError(t, err)
			// No update should have been made because the orbit enrolled path results in an immediately linked hostUUID->windows enrollment
			require.False(t, updated)
			err = s.ds.SetOrUpdateMDMData(ctx, host.ID, meta.isServer, true, s.server.URL, false, fleet.WellKnownMDMFleet, "", false)
			require.NoError(t, err)
		} else {
			host = createOrbitEnrolledHost(t, meta.os, meta.suffix, s.ds)
			createDeviceTokenForHost(t, s.ds, host.ID, meta.suffix)

			serverURL := "https://example.com"
			err := s.ds.SetOrUpdateMDMData(ctx, host.ID, meta.isServer, meta.enrolledName != "", serverURL, false, meta.enrolledName, "", false)
			require.NoError(t, err)
		}

		if meta.teamID != nil {
			err = s.ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(meta.teamID, []uint{host.ID}))
			require.NoError(t, err)
		}
		hostsBySuffix[meta.suffix] = host
	}

	// get the orbit config for each host, verify that only the expected ones
	// receive the "needs enrollment to Windows MDM" notification.
	for _, meta := range metadataHosts {
		var resp orbitGetConfigResponse
		s.DoJSON("POST", "/api/fleet/orbit/config",
			json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *hostsBySuffix[meta.suffix].OrbitNodeKey)),
			http.StatusOK, &resp)
		require.Equal(t, meta.shouldEnroll, resp.Notifications.NeedsProgrammaticWindowsMDMEnrollment)
		require.False(t, resp.Notifications.NeedsProgrammaticWindowsMDMUnenrollment)
		require.False(t, resp.Notifications.NeedsMDMMigration)
		if meta.shouldEnroll {
			require.Contains(t, resp.Notifications.WindowsMDMDiscoveryEndpoint, microsoft_mdm.MDE2DiscoveryPath)
		} else {
			require.Empty(t, resp.Notifications.WindowsMDMDiscoveryEndpoint)
		}
	}

	// enable Windows MDM manual enrollment
	acResp = appConfigResponse{}
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
		"mdm": { "enable_turn_on_windows_mdm_manually": true }
  }`), http.StatusOK, &acResp)
	assert.True(t, acResp.MDM.WindowsEnabledAndConfigured)
	assert.True(t, acResp.MDM.EnableTurnOnWindowsMDMManually)
	// No hosts should be told to enroll or unenroll because (un)enrollment is all done by the end-user
	for _, meta := range metadataHosts {
		var resp orbitGetConfigResponse
		s.DoJSON("POST", "/api/fleet/orbit/config",
			json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *hostsBySuffix[meta.suffix].OrbitNodeKey)),
			http.StatusOK, &resp)
		require.Equal(t, false, resp.Notifications.NeedsProgrammaticWindowsMDMEnrollment)
		require.Equal(t, false, resp.Notifications.NeedsMDMMigration)
		require.False(t, resp.Notifications.NeedsProgrammaticWindowsMDMUnenrollment)
		require.Empty(t, resp.Notifications.WindowsMDMDiscoveryEndpoint)
	}

	// enable Windows MDM migration
	acResp = appConfigResponse{}
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
		"mdm": { "windows_migration_enabled": true, "enable_turn_on_windows_mdm_manually": false }
  }`), http.StatusOK, &acResp)
	assert.True(t, acResp.MDM.WindowsEnabledAndConfigured)
	assert.True(t, acResp.MDM.WindowsMigrationEnabled)
	s.lastActivityMatches(fleet.ActivityTypeEnabledWindowsMDMMigration{}.ActivityName(), `{}`, 0)

	// get the orbit config for each host, verify that only the expected ones
	// receive the "needs enrollment to Windows MDM" and "needs migration" notifications.
	// They still get enrollment notifications as we have not proceeded with enrollment.
	for _, meta := range metadataHosts {
		var resp orbitGetConfigResponse
		s.DoJSON("POST", "/api/fleet/orbit/config",
			json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *hostsBySuffix[meta.suffix].OrbitNodeKey)),
			http.StatusOK, &resp)
		require.Equal(t, meta.shouldEnroll, resp.Notifications.NeedsProgrammaticWindowsMDMEnrollment)
		require.Equal(t, meta.shouldMigrate, resp.Notifications.NeedsMDMMigration)
		require.False(t, resp.Notifications.NeedsProgrammaticWindowsMDMUnenrollment)
		if meta.shouldEnroll {
			require.Contains(t, resp.Notifications.WindowsMDMDiscoveryEndpoint, microsoft_mdm.MDE2DiscoveryPath)
		} else {
			require.Empty(t, resp.Notifications.WindowsMDMDiscoveryEndpoint)
		}
	}

	// turn on MDM for another host
	orbitHost, _ := createWindowsHostThenEnrollMDM(s.ds, s.server.URL, t)

	// disable Microsoft MDM
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
		"mdm": { "windows_enabled_and_configured": false }
  }`), http.StatusOK, &acResp)
	assert.False(t, acResp.MDM.WindowsEnabledAndConfigured)
	assert.False(t, acResp.MDM.WindowsMigrationEnabled)
	s.lastActivityOfTypeMatches(fleet.ActivityTypeDisabledWindowsMDM{}.ActivityName(), `{}`, 0)

	// get the orbit config for that MDM-enrolled host returns true for the
	// unenrollment notification
	var resp orbitGetConfigResponse
	s.DoJSON("POST", "/api/fleet/orbit/config",
		json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *orbitHost.OrbitNodeKey)),
		http.StatusOK, &resp)
	require.True(t, resp.Notifications.NeedsProgrammaticWindowsMDMUnenrollment)
	require.False(t, resp.Notifications.NeedsProgrammaticWindowsMDMEnrollment)
	require.False(t, resp.Notifications.NeedsMDMMigration)
	require.Empty(t, resp.Notifications.WindowsMDMDiscoveryEndpoint)

	// get the orbit config for each host, only the fleet-enrolled ones get the unenrollment,
	// and none get enrollment/migration (because MDM is now off).
	for _, meta := range metadataHosts {
		var resp orbitGetConfigResponse
		s.DoJSON("POST", "/api/fleet/orbit/config",
			json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *hostsBySuffix[meta.suffix].OrbitNodeKey)),
			http.StatusOK, &resp)
		require.False(t, resp.Notifications.NeedsProgrammaticWindowsMDMEnrollment)
		require.False(t, resp.Notifications.NeedsMDMMigration)
		if meta.enrolledName == fleet.WellKnownMDMFleet {
			require.True(t, resp.Notifications.NeedsProgrammaticWindowsMDMUnenrollment)
		} else {
			require.False(t, resp.Notifications.NeedsProgrammaticWindowsMDMUnenrollment)
		}
		require.Empty(t, resp.Notifications.WindowsMDMDiscoveryEndpoint)
	}
}

func (s *integrationMDMTestSuite) TestOrbitConfigNudgeSettings() {
	t := s.T()

	// ensure the config is empty before starting
	s.applyConfig([]byte(`
  mdm:
    macos_updates:
      deadline: ""
      minimum_version: ""
 `))

	var resp orbitGetConfigResponse
	// missing orbit key
	s.DoJSON("POST", "/api/fleet/orbit/config", nil, http.StatusUnauthorized, &resp)

	// nudge config is empty if macos_updates is not set, and Windows MDM notifications are unset
	h := createOrbitEnrolledHost(t, "darwin", "h", s.ds)

	err := s.ds.UpdateHostOperatingSystem(context.Background(), h.ID, fleet.OperatingSystem{Platform: "darwin", Version: "12.0"})
	require.NoError(t, err)

	resp = orbitGetConfigResponse{}
	s.DoJSON("POST", "/api/fleet/orbit/config", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *h.OrbitNodeKey)), http.StatusOK, &resp)
	require.Empty(t, resp.NudgeConfig)
	require.False(t, resp.Notifications.NeedsProgrammaticWindowsMDMEnrollment)
	require.Empty(t, resp.Notifications.WindowsMDMDiscoveryEndpoint)
	require.False(t, resp.Notifications.NeedsProgrammaticWindowsMDMUnenrollment)

	// set macos_updates
	s.applyConfig([]byte(`
  mdm:
    macos_updates:
      deadline: 2022-01-04
      minimum_version: 12.1.3
 `))

	// still empty if MDM is turned off for the host
	resp = orbitGetConfigResponse{}
	s.DoJSON("POST", "/api/fleet/orbit/config", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *h.OrbitNodeKey)), http.StatusOK, &resp)
	require.Empty(t, resp.NudgeConfig)

	// turn on MDM features
	mdmDevice := mdmtest.NewTestMDMClientAppleDirect(mdmtest.AppleEnrollInfo{
		SCEPChallenge: s.scepChallenge,
		SCEPURL:       s.server.URL + apple_mdm.SCEPPath,
		MDMURL:        s.server.URL + apple_mdm.MDMPath,
	}, "MacBookPro16,1")
	mdmDevice.SerialNumber = h.HardwareSerial
	mdmDevice.UUID = h.UUID
	err = mdmDevice.Enroll()
	require.NoError(t, err)

	resp = orbitGetConfigResponse{}
	s.DoJSON("POST", "/api/fleet/orbit/config", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *h.OrbitNodeKey)), http.StatusOK, &resp)
	wantCfg, err := fleet.NewNudgeConfig(fleet.AppleOSUpdateSettings{Deadline: optjson.SetString("2022-01-04"), MinimumVersion: optjson.SetString("12.1.3")})
	require.NoError(t, err)
	require.Equal(t, wantCfg, resp.NudgeConfig)
	require.Equal(t, wantCfg.OSVersionRequirements[0].RequiredInstallationDate.String(), "2022-01-04 20:00:00 +0000 UTC")

	// create a team with an empty macos_updates config
	team, err := s.ds.NewTeam(context.Background(), &fleet.Team{
		ID:          4827,
		Name:        "team1_" + t.Name(),
		Description: "desc team1_" + t.Name(),
	})
	require.NoError(t, err)
	s.assertMacOSDeclarationsByName(&team.ID, servermdm.FleetMacOSUpdatesProfileName, false)

	// add the host to the team
	err = s.ds.AddHostsToTeam(context.Background(), fleet.NewAddHostsToTeamParams(&team.ID, []uint{h.ID}))
	require.NoError(t, err)

	// NudgeConfig should be empty
	resp = orbitGetConfigResponse{}
	s.DoJSON("POST", "/api/fleet/orbit/config", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *h.OrbitNodeKey)), http.StatusOK, &resp)
	require.Empty(t, resp.NudgeConfig)
	require.Equal(t, wantCfg.OSVersionRequirements[0].RequiredInstallationDate.String(), "2022-01-04 20:00:00 +0000 UTC")

	// modify the team config, add macos_updates config
	var tmResp teamResponse
	s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), fleet.TeamPayload{
		MDM: &fleet.TeamPayloadMDM{
			MacOSUpdates: &fleet.AppleOSUpdateSettings{
				Deadline:       optjson.SetString("1992-01-01"),
				MinimumVersion: optjson.SetString("13.1.1"),
			},
		},
	}, http.StatusOK, &tmResp)
	s.assertMacOSDeclarationsByName(&team.ID, servermdm.FleetMacOSUpdatesProfileName, true)

	resp = orbitGetConfigResponse{}
	s.DoJSON("POST", "/api/fleet/orbit/config", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *h.OrbitNodeKey)), http.StatusOK, &resp)
	wantCfg, err = fleet.NewNudgeConfig(fleet.AppleOSUpdateSettings{Deadline: optjson.SetString("1992-01-01"), MinimumVersion: optjson.SetString("13.1.1")})
	require.NoError(t, err)
	require.Equal(t, wantCfg, resp.NudgeConfig)
	require.Equal(t, wantCfg.OSVersionRequirements[0].RequiredInstallationDate.String(), "1992-01-01 20:00:00 +0000 UTC")

	// create a new host, still receives the global config
	h2 := createOrbitEnrolledHost(t, "darwin", "h2", s.ds)
	mdmDevice = mdmtest.NewTestMDMClientAppleDirect(mdmtest.AppleEnrollInfo{
		SCEPChallenge: s.scepChallenge,
		SCEPURL:       s.server.URL + apple_mdm.SCEPPath,
		MDMURL:        s.server.URL + apple_mdm.MDMPath,
	}, "MacBookPro16,1")
	mdmDevice.SerialNumber = h2.HardwareSerial
	mdmDevice.UUID = h2.UUID
	err = mdmDevice.Enroll()
	require.NoError(t, err)

	err = s.ds.UpdateHostOperatingSystem(context.Background(), h2.ID, fleet.OperatingSystem{Platform: "darwin", Version: "12.0"})
	require.NoError(t, err)

	resp = orbitGetConfigResponse{}
	s.DoJSON("POST", "/api/fleet/orbit/config", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *h2.OrbitNodeKey)), http.StatusOK, &resp)
	wantCfg, err = fleet.NewNudgeConfig(fleet.AppleOSUpdateSettings{Deadline: optjson.SetString("2022-01-04"), MinimumVersion: optjson.SetString("12.1.3")})
	require.NoError(t, err)
	require.Equal(t, wantCfg, resp.NudgeConfig)
	require.Equal(t, wantCfg.OSVersionRequirements[0].RequiredInstallationDate.String(), "2022-01-04 20:00:00 +0000 UTC")

	// host on macos > 14, shouldn't be receiving nudge configs
	h3 := createOrbitEnrolledHost(t, "darwin", "h3", s.ds)

	mdmDevice = mdmtest.NewTestMDMClientAppleDirect(mdmtest.AppleEnrollInfo{
		SCEPChallenge: s.scepChallenge,
		SCEPURL:       s.server.URL + apple_mdm.SCEPPath,
		MDMURL:        s.server.URL + apple_mdm.MDMPath,
	}, "MacBookPro16,1")
	mdmDevice.SerialNumber = h3.HardwareSerial
	mdmDevice.UUID = h3.UUID
	err = mdmDevice.Enroll()
	require.NoError(t, err)

	err = s.ds.UpdateHostOperatingSystem(context.Background(), h3.ID, fleet.OperatingSystem{Platform: "darwin", Version: "14.1"})
	require.NoError(t, err)

	resp = orbitGetConfigResponse{}
	s.DoJSON("POST", "/api/fleet/orbit/config", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *h3.OrbitNodeKey)), http.StatusOK, &resp)
	require.Nil(t, resp.NudgeConfig)

	// host is available for nudge, but has not had details query run
	// yet, so we don't know the os version.
	h4 := createOrbitEnrolledHost(t, "darwin", "h4", s.ds)

	mdmDevice = mdmtest.NewTestMDMClientAppleDirect(mdmtest.AppleEnrollInfo{
		SCEPChallenge: s.scepChallenge,
		SCEPURL:       s.server.URL + apple_mdm.SCEPPath,
		MDMURL:        s.server.URL + apple_mdm.MDMPath,
	}, "MacBookPro16,1")
	mdmDevice.SerialNumber = h4.HardwareSerial
	mdmDevice.UUID = h4.UUID
	err = mdmDevice.Enroll()
	require.NoError(t, err)

	resp = orbitGetConfigResponse{}
	s.DoJSON("POST", "/api/fleet/orbit/config", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *h4.OrbitNodeKey)), http.StatusOK, &resp)
	require.Nil(t, resp.NudgeConfig)
}

func (s *integrationMDMTestSuite) TestValidDiscoveryRequest() {
	t := s.T()
	// Preparing the Discovery Request message. We are testing all versions Fleet claims to support
	for _, requestVersion := range syncml.SupportedEnrollmentVersions {
		requestBytes := []byte(`
		 <s:Envelope xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:s="http://www.w3.org/2003/05/soap-envelope">
		   <s:Header>
		     <a:Action s:mustUnderstand="1">http://schemas.microsoft.com/windows/management/2012/01/enrollment/IDiscoveryService/Discover</a:Action>
		     <a:MessageID>urn:uuid:148132ec-a575-4322-b01b-6172a9cf8478</a:MessageID>
		     <a:ReplyTo>
		       <a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
		     </a:ReplyTo>
		     <a:To s:mustUnderstand="1">https://mdmwindows.com:443/EnrollmentServer/Discovery.svc</a:To>
		   </s:Header>
		   <s:Body>
		     <Discover xmlns="http://schemas.microsoft.com/windows/management/2012/01/enrollment">
		       <request xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
		         <EmailAddress>demo@mdmwindows.com</EmailAddress>
		         <RequestVersion>` + requestVersion + `</RequestVersion>
		         <DeviceType>CIMClient_Windows</DeviceType>
		         <ApplicationVersion>6.2.9200.2965</ApplicationVersion>
		         <OSEdition>48</OSEdition>
		         <AuthPolicies>
		           <AuthPolicy>OnPremise</AuthPolicy>
		           <AuthPolicy>Federated</AuthPolicy>
		         </AuthPolicies>
		       </request>
		     </Discover>
		   </s:Body>
		 </s:Envelope>`)

		resp := s.DoRaw("POST", microsoft_mdm.MDE2DiscoveryPath, requestBytes, http.StatusOK)

		resBytes, err := io.ReadAll(resp.Body)
		require.NoError(t, err)

		require.Contains(t, resp.Header["Content-Type"], syncml.SoapContentType)

		// Checking if SOAP response can be unmarshalled to an golang type
		var xmlType interface{}
		err = xml.Unmarshal(resBytes, &xmlType)
		require.NoError(t, err)

		// Checking if SOAP response contains a valid DiscoveryResponse message
		resSoapMsg := string(resBytes)
		require.True(t, s.isXMLTagPresent("DiscoverResult", resSoapMsg))
		require.True(t, s.isXMLTagContentPresent("AuthPolicy", resSoapMsg))
		require.True(t, s.isXMLTagContentPresent("EnrollmentVersion", resSoapMsg))
		require.True(t, s.isXMLTagContentPresent("EnrollmentPolicyServiceUrl", resSoapMsg))
		require.True(t, s.isXMLTagContentPresent("EnrollmentServiceUrl", resSoapMsg))
	}
}

func (s *integrationMDMTestSuite) TestInvalidDiscoveryRequest() {
	t := s.T()

	// Preparing the Discovery Request message
	requestBytes := []byte(`
		 <s:Envelope xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:s="http://www.w3.org/2003/05/soap-envelope">
		   <s:Header>
		     <a:Action s:mustUnderstand="1">http://schemas.microsoft.com/windows/management/2012/01/enrollment/IDiscoveryService/Discover</a:Action>
		     <a:ReplyTo>
		       <a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
		     </a:ReplyTo>
		     <a:To s:mustUnderstand="1">https://mdmwindows.com:443/EnrollmentServer/Discovery.svc</a:To>
		   </s:Header>
		   <s:Body>
		     <Discover xmlns="http://schemas.microsoft.com/windows/management/2012/01/enrollment">
		       <request xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
		         <EmailAddress>demo@mdmwindows.com</EmailAddress>
		         <RequestVersion>5.0</RequestVersion>
		         <DeviceType>CIMClient_Windows</DeviceType>
		         <ApplicationVersion>6.2.9200.2965</ApplicationVersion>
		         <OSEdition>48</OSEdition>
		         <AuthPolicies>
		           <AuthPolicy>OnPremise</AuthPolicy>
		           <AuthPolicy>Federated</AuthPolicy>
		         </AuthPolicies>
		       </request>
		     </Discover>
		   </s:Body>
		 </s:Envelope>`)

	resp := s.DoRaw("POST", microsoft_mdm.MDE2DiscoveryPath, requestBytes, http.StatusOK)

	resBytes, err := io.ReadAll(resp.Body)
	require.NoError(t, err)

	require.Contains(t, resp.Header["Content-Type"], syncml.SoapContentType)

	// Checking if response can be unmarshalled to an golang type
	var xmlType interface{}
	err = xml.Unmarshal(resBytes, &xmlType)
	require.NoError(t, err)

	// Checking if SOAP response contains a valid SoapFault message
	resSoapMsg := string(resBytes)

	require.True(t, s.isXMLTagPresent("s:fault", resSoapMsg))
	require.True(t, s.isXMLTagContentPresent("s:value", resSoapMsg))
	require.True(t, s.isXMLTagContentPresent("s:text", resSoapMsg))
	require.True(t, s.checkIfXMLTagContains("s:text", "invalid SOAP header: Header.MessageID", resSoapMsg))
}

func (s *integrationMDMTestSuite) TestNoEmailDiscoveryRequest() {
	t := s.T()

	// Preparing the Discovery Request message
	requestBytes := []byte(`
		 <s:Envelope xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:s="http://www.w3.org/2003/05/soap-envelope">
		   <s:Header>
		     <a:Action s:mustUnderstand="1">http://schemas.microsoft.com/windows/management/2012/01/enrollment/IDiscoveryService/Discover</a:Action>
		     <a:MessageID>urn:uuid:148132ec-a575-4322-b01b-6172a9cf8478</a:MessageID>
		     <a:ReplyTo>
		       <a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
		     </a:ReplyTo>
		     <a:To s:mustUnderstand="1">https://mdmwindows.com:443/EnrollmentServer/Discovery.svc</a:To>
		   </s:Header>
		   <s:Body>
		     <Discover xmlns="http://schemas.microsoft.com/windows/management/2012/01/enrollment">
		       <request xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
		         <EmailAddress></EmailAddress>
		         <RequestVersion>5.0</RequestVersion>
		         <DeviceType>CIMClient_Windows</DeviceType>
		         <ApplicationVersion>6.2.9200.2965</ApplicationVersion>
		         <OSEdition>48</OSEdition>
		         <AuthPolicies>
		           <AuthPolicy>OnPremise</AuthPolicy>
		           <AuthPolicy>Federated</AuthPolicy>
		         </AuthPolicies>
		       </request>
		     </Discover>
		   </s:Body>
		 </s:Envelope>`)

	resp := s.DoRaw("POST", microsoft_mdm.MDE2DiscoveryPath, requestBytes, http.StatusOK)

	resBytes, err := io.ReadAll(resp.Body)
	require.NoError(t, err)

	require.Contains(t, resp.Header["Content-Type"], syncml.SoapContentType)

	// Checking if SOAP response can be unmarshalled to an golang type
	var xmlType interface{}
	err = xml.Unmarshal(resBytes, &xmlType)
	require.NoError(t, err)

	// Checking if SOAP response contains a valid DiscoveryResponse message
	resSoapMsg := string(resBytes)
	require.True(t, s.isXMLTagPresent("DiscoverResult", resSoapMsg))
	require.True(t, s.isXMLTagContentPresent("AuthPolicy", resSoapMsg))
	require.True(t, s.isXMLTagContentPresent("EnrollmentVersion", resSoapMsg))
	require.True(t, s.isXMLTagContentPresent("EnrollmentPolicyServiceUrl", resSoapMsg))
	require.True(t, s.isXMLTagContentPresent("EnrollmentServiceUrl", resSoapMsg))
	require.True(t, !s.isXMLTagContentPresent("AuthenticationServiceUrl", resSoapMsg))
}

func (s *integrationMDMTestSuite) TestValidGetPoliciesRequestWithDeviceToken() {
	t := s.T()

	// create a new Host to get the UUID on the DB
	windowsHost := createOrbitEnrolledHost(t, "windows", "h1", s.ds)

	// Preparing the GetPolicies Request message
	encodedBinToken, err := fleet.GetEncodedBinarySecurityToken(fleet.WindowsMDMProgrammaticEnrollmentType, *windowsHost.OrbitNodeKey)
	require.NoError(t, err)

	requestBytes, err := s.newGetPoliciesMsg(true, encodedBinToken)
	require.NoError(t, err)

	resp := s.DoRaw("POST", microsoft_mdm.MDE2PolicyPath, requestBytes, http.StatusOK)

	resBytes, err := io.ReadAll(resp.Body)
	require.NoError(t, err)

	require.Contains(t, resp.Header["Content-Type"], syncml.SoapContentType)

	// Checking if SOAP response can be unmarshalled to an golang type
	var xmlType interface{}
	err = xml.Unmarshal(resBytes, &xmlType)
	require.NoError(t, err)

	// Checking if SOAP response contains a valid GetPoliciesResponse message
	resSoapMsg := string(resBytes)
	require.True(t, s.isXMLTagPresent("GetPoliciesResponse", resSoapMsg))
	require.True(t, s.isXMLTagPresent("policyOIDReference", resSoapMsg))
	require.True(t, s.isXMLTagPresent("oIDReferenceID", resSoapMsg))
	require.True(t, s.isXMLTagContentPresent("validityPeriodSeconds", resSoapMsg))
	require.True(t, s.isXMLTagContentPresent("renewalPeriodSeconds", resSoapMsg))
	require.True(t, s.isXMLTagContentPresent("minimalKeyLength", resSoapMsg))
}

func (s *integrationMDMTestSuite) TestValidGetPoliciesRequestWithAzureToken() {
	t := s.T()

	// Preparing the GetPolicies Request message with Azure JWT token
	azureADTok := "ZXlKMGVYQWlPaUpLVjFRaUxDSmhiR2NpT2lKU1V6STFOaUlzSW5nMWRDSTZJaTFMU1ROUk9XNU9VamRpVW05bWVHMWxXbTlZY1dKSVdrZGxkeUlzSW10cFpDSTZJaTFMU1ROUk9XNU9VamRpVW05bWVHMWxXbTlZY1dKSVdrZGxkeUo5LmV5SmhkV1FpT2lKb2RIUndjem92TDIxaGNtTnZjMnhoWW5NdWIzSm5MeUlzSW1semN5STZJbWgwZEhCek9pOHZjM1J6TG5kcGJtUnZkM011Ym1WMEwyWmhaVFZqTkdZekxXWXpNVGd0TkRRNE15MWlZelptTFRjMU9UVTFaalJoTUdFM01pOGlMQ0pwWVhRaU9qRTJPRGt4TnpBNE5UZ3NJbTVpWmlJNk1UWTRPVEUzTURnMU9Dd2laWGh3SWpveE5qZzVNVGMxTmpZeExDSmhZM0lpT2lJeElpd2lZV2x2SWpvaVFWUlJRWGt2T0ZSQlFVRkJOV2gwUTNFMGRERjNjbHBwUTIxQmVEQlpWaTloZGpGTVMwRkRPRXM1Vm10SGVtNUdXVGxzTUZoYWVrZHVha2N6VVRaMWVIUldNR3QxT1hCeFJXdFRZeUlzSW1GdGNpSTZXeUp3ZDJRaUxDSnljMkVpWFN3aVlYQndhV1FpT2lJeU9XUTVaV1E1T0MxaE5EWTVMVFExTXpZdFlXUmxNaTFtT1RneFltTXhaRFl3TldVaUxDSmhjSEJwWkdGamNpSTZJakFpTENKa1pYWnBZMlZwWkNJNkltRXhNMlkzWVdVd0xURXpPR0V0TkdKaU1pMDVNalF5TFRka09USXlaVGRqTkdGak15SXNJbWx3WVdSa2NpSTZJakU0Tmk0eE1pNHhPRGN1TWpZaUxDSnVZVzFsSWpvaVZHVnpkRTFoY21OdmMweGhZbk1pTENKdmFXUWlPaUpsTTJNMU5XVmtZeTFqTXpRNExUUTBNVFl0T0dZd05TMHlOVFJtWmpNd05qVmpOV1VpTENKd2QyUmZkWEpzSWpvaWFIUjBjSE02THk5d2IzSjBZV3d1YldsamNtOXpiMlowYjI1c2FXNWxMbU52YlM5RGFHRnVaMlZRWVhOemQyOXlaQzVoYzNCNElpd2ljbWdpT2lJd0xrRldTVUU0T0ZSc0xXaHFlbWN3VXpoaU0xZFdXREJ2UzJOdFZGRXpTbHB1ZUUxa1QzQTNUbVZVVm5OV2FYVkhOa0ZRYnk0aUxDSnpZM0FpT2lKdFpHMWZaR1ZzWldkaGRHbHZiaUlzSW5OMVlpSTZJa1pTUTJ4RldURk9ObXR2ZEdWblMzcFplV0pFTjJkdFdGbGxhVTVIUkZrd05FSjJOV3R6ZDJGeGJVRWlMQ0owYVdRaU9pSm1ZV1UxWXpSbU15MW1NekU0TFRRME9ETXRZbU0yWmkwM05UazFOV1kwWVRCaE56SWlMQ0oxYm1seGRXVmZibUZ0WlNJNkluUmxjM1JBYldGeVkyOXpiR0ZpY3k1dmNtY2lMQ0oxY0c0aU9pSjBaWE4wUUcxaGNtTnZjMnhoWW5NdWIzSm5JaXdpZFhScElqb2lNVGg2WkVWSU5UZFRSWFZyYWpseGJqRm9aMlJCUVNJc0luWmxjaUk2SWpFdU1DSjkuVG1FUlRsZktBdWo5bTVvQUc2UTBRblV4VEFEaTNFamtlNHZ3VXo3UTdqUUFVZVZGZzl1U0pzUXNjU2hFTXVxUmQzN1R2VlpQanljdEVoRFgwLVpQcEVVYUlSempuRVEyTWxvc21SZURYZzhrYkhNZVliWi1jb0ZucDEyQkVpQnpJWFBGZnBpaU1GRnNZZ0hSSF9tSWxwYlBlRzJuQ2p0LTZSOHgzYVA5QS1tM0J3eV91dnV0WDFNVEVZRmFsekhGa04wNWkzbjZRcjhURnlJQ1ZUYW5OanlkMjBBZFRMbHJpTVk0RVBmZzRaLThVVTctZkcteElycWVPUmVWTnYwOUFHV192MDd6UkVaNmgxVk9tNl9nelRGcElVVURuZFdabnFLTHlySDlkdkF3WnFFSG1HUmlTNElNWnRFdDJNTkVZSnhDWHhlSi1VbWZJdV9tUVhKMW9R"
	requestBytes, err := s.newGetPoliciesMsg(false, azureADTok)
	require.NoError(t, err)

	resp := s.DoRaw("POST", microsoft_mdm.MDE2PolicyPath, requestBytes, http.StatusOK)

	resBytes, err := io.ReadAll(resp.Body)
	require.NoError(t, err)

	require.Contains(t, resp.Header["Content-Type"], syncml.SoapContentType)

	// Checking if SOAP response can be unmarshalled to an golang type
	var xmlType interface{}
	err = xml.Unmarshal(resBytes, &xmlType)
	require.NoError(t, err)

	// Checking if SOAP response contains a valid GetPoliciesResponse message
	resSoapMsg := string(resBytes)
	require.True(t, s.isXMLTagPresent("GetPoliciesResponse", resSoapMsg))
	require.True(t, s.isXMLTagPresent("policyOIDReference", resSoapMsg))
	require.True(t, s.isXMLTagPresent("oIDReferenceID", resSoapMsg))
	require.True(t, s.isXMLTagContentPresent("validityPeriodSeconds", resSoapMsg))
	require.True(t, s.isXMLTagContentPresent("renewalPeriodSeconds", resSoapMsg))
	require.True(t, s.isXMLTagContentPresent("minimalKeyLength", resSoapMsg))
}

func (s *integrationMDMTestSuite) TestGetPoliciesRequestWithInvalidUUID() {
	t := s.T()

	// create a new Host to get the UUID on the DB
	_, err := s.ds.NewHost(context.Background(), &fleet.Host{
		ID:            1,
		OsqueryHostID: ptr.String("Desktop-ABCQWE"),
		NodeKey:       ptr.String("Desktop-ABCQWE"),
		UUID:          uuid.New().String(),
		Hostname:      fmt.Sprintf("%sfoo.local.not.enrolled", s.T().Name()),
		Platform:      "windows",
	})
	require.NoError(t, err)

	// Preparing the GetPolicies Request message
	encodedBinToken, err := fleet.GetEncodedBinarySecurityToken(fleet.WindowsMDMProgrammaticEnrollmentType, "not_exists")
	require.NoError(t, err)

	requestBytes, err := s.newGetPoliciesMsg(true, encodedBinToken)
	require.NoError(t, err)

	resp := s.DoRaw("POST", microsoft_mdm.MDE2PolicyPath, requestBytes, http.StatusOK)

	resBytes, err := io.ReadAll(resp.Body)
	require.NoError(t, err)

	require.Contains(t, resp.Header["Content-Type"], syncml.SoapContentType)

	// Checking if SOAP response can be unmarshalled to an golang type
	var xmlType interface{}
	err = xml.Unmarshal(resBytes, &xmlType)
	require.NoError(t, err)

	// Checking if SOAP response contains a valid SoapFault message
	resSoapMsg := string(resBytes)
	require.True(t, s.isXMLTagPresent("s:fault", resSoapMsg))
	require.True(t, s.isXMLTagContentPresent("s:value", resSoapMsg))
	require.True(t, s.isXMLTagContentPresent("s:text", resSoapMsg))
	require.True(t, s.checkIfXMLTagContains("s:text", "host data cannot be found", resSoapMsg))
}

func (s *integrationMDMTestSuite) TestGetPoliciesRequestWithNotElegibleHost() {
	t := s.T()

	// create a new Host to get the UUID on the DB
	linuxHost := createOrbitEnrolledHost(t, "linux", "h1", s.ds)

	// Preparing the GetPolicies Request message
	encodedBinToken, err := fleet.GetEncodedBinarySecurityToken(fleet.WindowsMDMProgrammaticEnrollmentType, *linuxHost.OrbitNodeKey)
	require.NoError(t, err)

	requestBytes, err := s.newGetPoliciesMsg(true, encodedBinToken)
	require.NoError(t, err)

	resp := s.DoRaw("POST", microsoft_mdm.MDE2PolicyPath, requestBytes, http.StatusOK)

	resBytes, err := io.ReadAll(resp.Body)
	require.NoError(t, err)

	require.Contains(t, resp.Header["Content-Type"], syncml.SoapContentType)

	// Checking if SOAP response can be unmarshalled to an golang type
	var xmlType interface{}
	err = xml.Unmarshal(resBytes, &xmlType)
	require.NoError(t, err)

	// Checking if SOAP response contains a valid SoapFault message
	resSoapMsg := string(resBytes)
	require.True(t, s.isXMLTagPresent("s:fault", resSoapMsg))
	require.True(t, s.isXMLTagContentPresent("s:value", resSoapMsg))
	require.True(t, s.isXMLTagContentPresent("s:text", resSoapMsg))
	require.True(t, s.checkIfXMLTagContains("s:text", "host is not elegible for Windows MDM enrollment", resSoapMsg))
}

func (s *integrationMDMTestSuite) TestValidRequestSecurityTokenRequestWithDeviceToken() {
	t := s.T()
	windowsHost := createOrbitEnrolledHost(t, "windows", "h1", s.ds)

	// Delete the host from the list of MDM enrolled devices if present
	_ = s.ds.MDMWindowsDeleteEnrolledDeviceOnReenrollment(context.Background(), windowsHost.UUID)

	// Preparing the RequestSecurityToken Request message
	encodedBinToken, err := fleet.GetEncodedBinarySecurityToken(fleet.WindowsMDMProgrammaticEnrollmentType, *windowsHost.OrbitNodeKey)
	require.NoError(t, err)

	requestBytes, err := s.newSecurityTokenMsg(encodedBinToken, true, false)
	require.NoError(t, err)

	resp := s.DoRaw("POST", microsoft_mdm.MDE2EnrollPath, requestBytes, http.StatusOK)

	resBytes, err := io.ReadAll(resp.Body)
	require.NoError(t, err)

	require.Contains(t, resp.Header["Content-Type"], syncml.SoapContentType)

	// Checking if SOAP response can be unmarshalled to an golang type
	var xmlType interface{}
	err = xml.Unmarshal(resBytes, &xmlType)
	require.NoError(t, err)

	// Checking if SOAP response contains a valid RequestSecurityTokenResponseCollection message
	resSoapMsg := string(resBytes)

	require.True(t, s.isXMLTagPresent("RequestSecurityTokenResponseCollection", resSoapMsg))
	require.True(t, s.isXMLTagPresent("DispositionMessage", resSoapMsg))
	require.True(t, s.isXMLTagContentPresent("TokenType", resSoapMsg))
	require.True(t, s.isXMLTagContentPresent("RequestID", resSoapMsg))
	require.True(t, s.isXMLTagContentPresent("BinarySecurityToken", resSoapMsg))

	// Checking if an activity was created for the enrollment
	s.lastActivityOfTypeMatches(
		fleet.ActivityTypeMDMEnrolled{}.ActivityName(),
		fmt.Sprintf(`{
			"mdm_platform": "microsoft",
			"host_serial": "%s",
			"installed_from_dep": false,
			"host_display_name": "%s",
			"enrollment_id": null,
			"platform": "windows"
		 }`, windowsHost.HardwareSerial, windowsHost.DisplayName()),
		0)

	expectedDeviceID := "AB157C3A18778F4FB21E2739066C1F27" // TODO: make the hard-coded deviceID in `s.newSecurityTokenMsg` configurable

	// Checking if the host uuid was set on mdm windows enrollments
	d, err := s.ds.MDMWindowsGetEnrolledDeviceWithDeviceID(context.Background(), expectedDeviceID)
	require.NoError(t, err)
	require.NotEmpty(t, d.HostUUID)
	require.Equal(t, windowsHost.UUID, d.HostUUID)
}

// TODO: Do we need integration tests for WindowsMDMAutomaticEnrollmentType flows?

func (s *integrationMDMTestSuite) TestValidRequestSecurityTokenRequestWithAzureToken() {
	t := s.T()

	// Preparing the SecurityToken Request message with Azure JWT token
	azureADTok := "ZXlKMGVYQWlPaUpLVjFRaUxDSmhiR2NpT2lKU1V6STFOaUlzSW5nMWRDSTZJaTFMU1ROUk9XNU9VamRpVW05bWVHMWxXbTlZY1dKSVdrZGxkeUlzSW10cFpDSTZJaTFMU1ROUk9XNU9VamRpVW05bWVHMWxXbTlZY1dKSVdrZGxkeUo5LmV5SmhkV1FpT2lKb2RIUndjem92TDIxaGNtTnZjMnhoWW5NdWIzSm5MeUlzSW1semN5STZJbWgwZEhCek9pOHZjM1J6TG5kcGJtUnZkM011Ym1WMEwyWmhaVFZqTkdZekxXWXpNVGd0TkRRNE15MWlZelptTFRjMU9UVTFaalJoTUdFM01pOGlMQ0pwWVhRaU9qRTJPRGt4TnpBNE5UZ3NJbTVpWmlJNk1UWTRPVEUzTURnMU9Dd2laWGh3SWpveE5qZzVNVGMxTmpZeExDSmhZM0lpT2lJeElpd2lZV2x2SWpvaVFWUlJRWGt2T0ZSQlFVRkJOV2gwUTNFMGRERjNjbHBwUTIxQmVEQlpWaTloZGpGTVMwRkRPRXM1Vm10SGVtNUdXVGxzTUZoYWVrZHVha2N6VVRaMWVIUldNR3QxT1hCeFJXdFRZeUlzSW1GdGNpSTZXeUp3ZDJRaUxDSnljMkVpWFN3aVlYQndhV1FpT2lJeU9XUTVaV1E1T0MxaE5EWTVMVFExTXpZdFlXUmxNaTFtT1RneFltTXhaRFl3TldVaUxDSmhjSEJwWkdGamNpSTZJakFpTENKa1pYWnBZMlZwWkNJNkltRXhNMlkzWVdVd0xURXpPR0V0TkdKaU1pMDVNalF5TFRka09USXlaVGRqTkdGak15SXNJbWx3WVdSa2NpSTZJakU0Tmk0eE1pNHhPRGN1TWpZaUxDSnVZVzFsSWpvaVZHVnpkRTFoY21OdmMweGhZbk1pTENKdmFXUWlPaUpsTTJNMU5XVmtZeTFqTXpRNExUUTBNVFl0T0dZd05TMHlOVFJtWmpNd05qVmpOV1VpTENKd2QyUmZkWEpzSWpvaWFIUjBjSE02THk5d2IzSjBZV3d1YldsamNtOXpiMlowYjI1c2FXNWxMbU52YlM5RGFHRnVaMlZRWVhOemQyOXlaQzVoYzNCNElpd2ljbWdpT2lJd0xrRldTVUU0T0ZSc0xXaHFlbWN3VXpoaU0xZFdXREJ2UzJOdFZGRXpTbHB1ZUUxa1QzQTNUbVZVVm5OV2FYVkhOa0ZRYnk0aUxDSnpZM0FpT2lKdFpHMWZaR1ZzWldkaGRHbHZiaUlzSW5OMVlpSTZJa1pTUTJ4RldURk9ObXR2ZEdWblMzcFplV0pFTjJkdFdGbGxhVTVIUkZrd05FSjJOV3R6ZDJGeGJVRWlMQ0owYVdRaU9pSm1ZV1UxWXpSbU15MW1NekU0TFRRME9ETXRZbU0yWmkwM05UazFOV1kwWVRCaE56SWlMQ0oxYm1seGRXVmZibUZ0WlNJNkluUmxjM1JBYldGeVkyOXpiR0ZpY3k1dmNtY2lMQ0oxY0c0aU9pSjBaWE4wUUcxaGNtTnZjMnhoWW5NdWIzSm5JaXdpZFhScElqb2lNVGg2WkVWSU5UZFRSWFZyYWpseGJqRm9aMlJCUVNJc0luWmxjaUk2SWpFdU1DSjkuVG1FUlRsZktBdWo5bTVvQUc2UTBRblV4VEFEaTNFamtlNHZ3VXo3UTdqUUFVZVZGZzl1U0pzUXNjU2hFTXVxUmQzN1R2VlpQanljdEVoRFgwLVpQcEVVYUlSempuRVEyTWxvc21SZURYZzhrYkhNZVliWi1jb0ZucDEyQkVpQnpJWFBGZnBpaU1GRnNZZ0hSSF9tSWxwYlBlRzJuQ2p0LTZSOHgzYVA5QS1tM0J3eV91dnV0WDFNVEVZRmFsekhGa04wNWkzbjZRcjhURnlJQ1ZUYW5OanlkMjBBZFRMbHJpTVk0RVBmZzRaLThVVTctZkcteElycWVPUmVWTnYwOUFHV192MDd6UkVaNmgxVk9tNl9nelRGcElVVURuZFdabnFLTHlySDlkdkF3WnFFSG1HUmlTNElNWnRFdDJNTkVZSnhDWHhlSi1VbWZJdV9tUVhKMW9R"
	requestBytes, err := s.newSecurityTokenMsg(azureADTok, false, false)
	require.NoError(t, err)

	resp := s.DoRaw("POST", microsoft_mdm.MDE2EnrollPath, requestBytes, http.StatusOK)

	resBytes, err := io.ReadAll(resp.Body)
	require.NoError(t, err)

	require.Contains(t, resp.Header["Content-Type"], syncml.SoapContentType)

	// Checking if SOAP response can be unmarshalled to an golang type
	var xmlType interface{}
	err = xml.Unmarshal(resBytes, &xmlType)
	require.NoError(t, err)

	// Checking if SOAP response contains a valid RequestSecurityTokenResponseCollection message
	resSoapMsg := string(resBytes)
	require.True(t, s.isXMLTagPresent("RequestSecurityTokenResponseCollection", resSoapMsg))
	require.True(t, s.isXMLTagPresent("DispositionMessage", resSoapMsg))
	require.True(t, s.isXMLTagContentPresent("TokenType", resSoapMsg))
	require.True(t, s.isXMLTagContentPresent("RequestID", resSoapMsg))
	require.True(t, s.isXMLTagContentPresent("BinarySecurityToken", resSoapMsg))

	// Checking if an activity was created for the enrollment
	s.lastActivityOfTypeMatches(
		fleet.ActivityTypeMDMEnrolled{}.ActivityName(),
		`{
			"mdm_platform": "microsoft",
			"host_serial": "",
			"installed_from_dep": false,
			"host_display_name": "DESKTOP-0C89RC0",
			"enrollment_id": null,
			"platform": "windows"
		 }`,
		0)

	expectedDeviceID := "AB157C3A18778F4FB21E2739066C1F27" // TODO: make the hard-coded deviceID in `s.newSecurityTokenMsg` configurable

	// Checking the host uuid was not set on mdm windows enrollments
	d, err := s.ds.MDMWindowsGetEnrolledDeviceWithDeviceID(context.Background(), expectedDeviceID)
	require.NoError(t, err)
	require.Empty(t, d.HostUUID)
}

func (s *integrationMDMTestSuite) TestInvalidRequestSecurityTokenRequestWithMissingAdditionalContext() {
	t := s.T()

	// create a new Host to get the UUID on the DB
	windowsHost := createOrbitEnrolledHost(t, "windows", "h1", s.ds)

	// Preparing the RequestSecurityToken Request message
	encodedBinToken, err := fleet.GetEncodedBinarySecurityToken(fleet.WindowsMDMProgrammaticEnrollmentType, *windowsHost.OrbitNodeKey)
	require.NoError(t, err)

	requestBytes, err := s.newSecurityTokenMsg(encodedBinToken, true, true)
	require.NoError(t, err)

	resp := s.DoRaw("POST", microsoft_mdm.MDE2EnrollPath, requestBytes, http.StatusOK)

	resBytes, err := io.ReadAll(resp.Body)
	require.NoError(t, err)

	require.Contains(t, resp.Header["Content-Type"], syncml.SoapContentType)

	// Checking if SOAP response can be unmarshalled to an golang type
	var xmlType interface{}
	err = xml.Unmarshal(resBytes, &xmlType)
	require.NoError(t, err)

	// Checking if SOAP response contains a valid SoapFault message
	resSoapMsg := string(resBytes)
	require.True(t, s.isXMLTagPresent("s:fault", resSoapMsg))
	require.True(t, s.isXMLTagContentPresent("s:value", resSoapMsg))
	require.True(t, s.isXMLTagContentPresent("s:text", resSoapMsg))
	require.True(t, s.checkIfXMLTagContains("s:text", "ContextItem item DeviceType is not present", resSoapMsg))
}

func (s *integrationMDMTestSuite) TestValidGetAuthRequest() {
	t := s.T()

	// Target Endpoint url with query params
	targetEndpointURL := microsoft_mdm.MDE2AuthPath + "?appru=ms-app%3A%2F%2Fwindows.immersivecontrolpanel&login_hint=demo%40mdmwindows.com"
	resp := s.DoRaw("GET", targetEndpointURL, nil, http.StatusOK)

	resBytes, err := io.ReadAll(resp.Body)
	require.NoError(t, err)
	require.Contains(t, resp.Header["Content-Type"], "text/html; charset=UTF-8")
	require.NotEmpty(t, resBytes)

	// Checking response content
	resContent := string(resBytes)
	require.Contains(t, resContent, "inputToken.name = 'wresult'")
	require.Contains(t, resContent, "form.action = \"ms-app://windows.immersivecontrolpanel\"")
	require.Contains(t, resContent, "performPost()")

	// Getting token content
	encodedToken := s.getRawTokenValue(resContent)
	require.NotEmpty(t, encodedToken)
}

func (s *integrationMDMTestSuite) TestInvalidGetAuthRequest() {
	t := s.T()

	// Target Endpoint url with no login_hit query param
	targetEndpointURL := microsoft_mdm.MDE2AuthPath + "?appru=ms-app%3A%2F%2Fwindows.immersivecontrolpanel"
	resp := s.DoRaw("GET", targetEndpointURL, nil, http.StatusInternalServerError)

	resBytes, err := io.ReadAll(resp.Body)
	resContent := string(resBytes)
	require.NoError(t, err)
	require.NotEmpty(t, resBytes)
	require.Contains(t, resContent, "forbidden")
}

func (s *integrationMDMTestSuite) TestValidGetTOC() {
	t := s.T()

	// hacky check to make sure the assets were built (they require `-tags full`
	// when building the tests otherwise this test will always fail due to a
	// panic in server/bindata package)
	defer func() {
		if panicVal := recover(); panicVal != nil {
			s, ok := panicVal.(string)
			if ok && strings.Contains(s, "Assets may not be used when running Fleet as a library") {
				t.Skip("skipping, test will fail due to assets not built (requires '-tags full')")
			}
		}
	}()
	_, _ = bindata.Asset("check if assets are build")

	resp := s.DoRaw("GET", microsoft_mdm.MDE2TOSPath+"?api-version=1.0&redirect_uri=ms-appx-web%3a%2f%2fMicrosoft.AAD.BrokerPlugin&client-request-id=f2cf3127-1e80-4d73-965d-42a3b84bdb40", nil, http.StatusOK)

	resBytes, err := io.ReadAll(resp.Body)
	require.NoError(t, err)

	require.Contains(t, resp.Header["Content-Type"], syncml.WebContainerContentType)

	resTOCcontent := string(resBytes)
	require.Contains(t, resTOCcontent, "Microsoft.AAD.BrokerPlugin")
	require.Contains(t, resTOCcontent, "IsAccepted=true")
	require.Contains(t, resTOCcontent, "OpaqueBlob=")
}

func (s *integrationMDMTestSuite) TestWindowsMDM() {
	t := s.T()
	orbitHost, d := createWindowsHostThenEnrollMDM(s.ds, s.server.URL, t)

	cmdOneUUID := uuid.New().String()
	commandOne := &fleet.MDMWindowsCommand{
		CommandUUID: cmdOneUUID,
		RawCommand: []byte(fmt.Sprintf(`
                     <Exec>
                       <CmdID>%s</CmdID>
                       <Item>
                         <Target>
                           <LocURI>./Device/Vendor/MSFT/Reboot/RebootNow</LocURI>
                         </Target>
                         <Meta>
                           <Format xmlns="syncml:metinf">null</Format>
                           <Type>text/plain</Type>
                         </Meta>
                         <Data></Data>
                       </Item>
                     </Exec>
		`, cmdOneUUID)),
		TargetLocURI: "./Device/Vendor/MSFT/Reboot/RebootNow",
	}
	err := s.ds.MDMWindowsInsertCommandForHosts(context.Background(), []string{orbitHost.UUID}, commandOne)
	require.NoError(t, err)

	cmds, err := d.StartManagementSession()
	require.NoError(t, err)
	// 2 Status + 1 Exec
	require.Len(t, cmds, 3)
	receivedCmd := cmds[cmdOneUUID]
	require.NotNil(t, receivedCmd)
	require.Equal(t, receivedCmd.Verb, fleet.CmdExec)
	require.Len(t, receivedCmd.Cmd.Items, 1)
	require.EqualValues(t, "./Device/Vendor/MSFT/Reboot/RebootNow", *receivedCmd.Cmd.Items[0].Target)

	msgID, err := d.GetCurrentMsgID()
	require.NoError(t, err)

	d.AppendResponse(fleet.SyncMLCmd{
		XMLName: xml.Name{Local: fleet.CmdStatus},
		MsgRef:  &msgID,
		CmdRef:  &cmdOneUUID,
		Cmd:     ptr.String("Exec"),
		Data:    ptr.String("200"),
		Items:   nil,
		CmdID:   fleet.CmdID{Value: uuid.NewString()},
	})
	cmds, err = d.SendResponse()
	require.NoError(t, err)
	// the ack of the message should be the only returned command
	require.Len(t, cmds, 1)

	cmdTwoUUID := uuid.New().String()
	commandTwo := &fleet.MDMWindowsCommand{
		CommandUUID: cmdTwoUUID,
		RawCommand: []byte(fmt.Sprintf(`
                    <Get>
                      <CmdID>%s</CmdID>
                      <Item>
                        <Target>
                          <LocURI>./Device/Vendor/MSFT/DMClient/Provider/DEMO%%20MDM/SignedEntDMID</LocURI>
                        </Target>
                      </Item>
                    </Get>
		`, cmdTwoUUID)),
		TargetLocURI: "./Device/Vendor/MSFT/DMClient/Provider/DEMO%%20MDM/SignedEntDMID",
	}
	err = s.ds.MDMWindowsInsertCommandForHosts(context.Background(), []string{orbitHost.UUID}, commandTwo)
	require.NoError(t, err)

	cmdThreeUUID := uuid.New().String()
	commandThree := &fleet.MDMWindowsCommand{
		CommandUUID: cmdThreeUUID,
		RawCommand: []byte(fmt.Sprintf(`
                    <Replace>
                       <CmdID>%s</CmdID>
                       <Item>
                         <Target>
                           <LocURI>./Device/Vendor/MSFT/DMClient/Provider/DEMO%%20MDM/SignedEntDMID</LocURI>
                         </Target>
                         <Meta>
                           <Type xmlns="syncml:metinf">text/plain</Type>
                           <Format xmlns="syncml:metinf">chr</Format>
                         </Meta>
                         <Data>1</Data>
                       </Item>
                    </Replace>
		`, cmdThreeUUID)),
		TargetLocURI: "./Device/Vendor/MSFT/DMClient/Provider/DEMO%%20MDM/SignedEntDMID",
	}
	err = s.ds.MDMWindowsInsertCommandForHosts(context.Background(), []string{orbitHost.UUID}, commandThree)
	require.NoError(t, err)

	cmdFourUUID := uuid.New().String()
	commandFour := &fleet.MDMWindowsCommand{
		CommandUUID: cmdFourUUID,
		RawCommand: []byte(fmt.Sprintf(`
                    <Add>
                       <CmdID>%s</CmdID>
                       <Item>
                         <Target>
                           <LocURI>./Vendor/MSFT/WiFi/Profile/MyNetwork/WlanXml</LocURI>
                         </Target>
                         <Meta>
                           <Type xmlns="syncml:metinf">text/plain</Type>
                           <Format xmlns="syncml:metinf">chr</Format>
                         </Meta>
                         <Data>
						   &lt;?xml version=&quot;1.0&quot;?&gt;&lt;WLANProfile
						   xmlns=&quot;http://contoso.com/provisioning/EapHostConfig&quot;&gt;&lt;EapMethod&gt;&lt;Type
						 </Data>
                       </Item>
                    </Add>
		`, cmdFourUUID)),
		TargetLocURI: "./Vendor/MSFT/WiFi/Profile/MyNetwork/WlanXml",
	}
	err = s.ds.MDMWindowsInsertCommandForHosts(context.Background(), []string{orbitHost.UUID}, commandFour)
	require.NoError(t, err)

	cmds, err = d.StartManagementSession()
	require.NoError(t, err)
	// two status + the three commands we enqueued
	require.Len(t, cmds, 5)
	receivedCmdTwo := cmds[cmdTwoUUID]
	require.NotNil(t, receivedCmdTwo)
	require.Equal(t, receivedCmdTwo.Verb, fleet.CmdGet)
	require.Len(t, receivedCmdTwo.Cmd.Items, 1)
	require.EqualValues(t, "./Device/Vendor/MSFT/DMClient/Provider/DEMO%20MDM/SignedEntDMID", *receivedCmdTwo.Cmd.Items[0].Target)

	receivedCmdThree := cmds[cmdThreeUUID]
	require.NotNil(t, receivedCmdThree)
	require.Equal(t, receivedCmdThree.Verb, fleet.CmdReplace)
	require.Len(t, receivedCmdThree.Cmd.Items, 1)
	require.EqualValues(t, "./Device/Vendor/MSFT/DMClient/Provider/DEMO%20MDM/SignedEntDMID", *receivedCmdThree.Cmd.Items[0].Target)

	receivedCmdFour := cmds[cmdFourUUID]
	require.NotNil(t, receivedCmdFour)
	require.Equal(t, receivedCmdFour.Verb, fleet.CmdAdd)
	require.Len(t, receivedCmdFour.Cmd.Items, 1)
	require.EqualValues(t, "./Vendor/MSFT/WiFi/Profile/MyNetwork/WlanXml", *receivedCmdFour.Cmd.Items[0].Target)

	// status 200 for command Two  (Get)
	d.AppendResponse(fleet.SyncMLCmd{
		XMLName: xml.Name{Local: fleet.CmdStatus},
		MsgRef:  &msgID,
		CmdRef:  &cmdTwoUUID,
		Cmd:     ptr.String("Get"),
		Data:    ptr.String("200"),
		Items:   nil,
		CmdID:   fleet.CmdID{Value: uuid.NewString()},
	})
	// results for command two (Get)
	cmdTwoRespUUID := uuid.NewString()
	d.AppendResponse(fleet.SyncMLCmd{
		XMLName: xml.Name{Local: fleet.CmdResults},
		MsgRef:  &msgID,
		CmdRef:  &cmdTwoUUID,
		Cmd:     ptr.String("Replace"),
		Data:    ptr.String("200"),
		Items: []fleet.CmdItem{
			{
				Source: ptr.String("./Device/Vendor/MSFT/DMClient/Provider/DEMO%20MDM/SignedEntDMID"),
				Data:   &fleet.RawXmlData{Content: "0"},
			},
		},
		CmdID: fleet.CmdID{Value: cmdTwoRespUUID},
	})
	// status 200 for command Three (Replace)
	d.AppendResponse(fleet.SyncMLCmd{
		XMLName: xml.Name{Local: fleet.CmdStatus},
		MsgRef:  &msgID,
		CmdRef:  &cmdThreeUUID,
		Cmd:     ptr.String("Replace"),
		Data:    ptr.String("200"),
		Items:   nil,
		CmdID:   fleet.CmdID{Value: uuid.NewString()},
	})
	// status 200 for command Four (Add)
	d.AppendResponse(fleet.SyncMLCmd{
		XMLName: xml.Name{Local: fleet.CmdStatus},
		MsgRef:  &msgID,
		CmdRef:  &cmdFourUUID,
		Cmd:     ptr.String("Add"),
		Data:    ptr.String("200"),
		Items:   nil,
		CmdID:   fleet.CmdID{Value: uuid.NewString()},
	})
	cmds, err = d.SendResponse()
	require.NoError(t, err)

	// the ack of the message should be the only returned command
	require.Len(t, cmds, 1)

	// check command results

	getCommandFullResult := func(cmdUUID string) []byte {
		var fullResult []byte
		mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
			return sqlx.GetContext(context.Background(), q, &fullResult, `
			SELECT raw_response
			FROM windows_mdm_responses wmr
			JOIN windows_mdm_command_results wmcr ON wmcr.response_id = wmr.id
			WHERE command_uuid = ?
			`, cmdUUID)
		})
		return fullResult
	}

	var getMDMCmdResp getMDMCommandResultsResponse
	s.DoJSON("GET", "/api/latest/fleet/commands/results", nil, http.StatusOK, &getMDMCmdResp, "command_uuid", cmdOneUUID)
	require.Len(t, getMDMCmdResp.Results, 1)
	require.NotZero(t, getMDMCmdResp.Results[0].UpdatedAt)
	getMDMCmdResp.Results[0].UpdatedAt = time.Time{}
	require.Equal(t, &fleet.MDMCommandResult{
		HostUUID:    orbitHost.UUID,
		CommandUUID: cmdOneUUID,
		Status:      "200",
		RequestType: "./Device/Vendor/MSFT/Reboot/RebootNow",
		Result:      getCommandFullResult(cmdOneUUID),
		Payload:     commandOne.RawCommand,
		Hostname:    orbitHost.Hostname,
	}, getMDMCmdResp.Results[0])

	s.DoJSON("GET", "/api/latest/fleet/commands/results", nil, http.StatusOK, &getMDMCmdResp, "command_uuid", cmdTwoUUID)
	require.Len(t, getMDMCmdResp.Results, 1)
	require.NotZero(t, getMDMCmdResp.Results[0].UpdatedAt)
	getMDMCmdResp.Results[0].UpdatedAt = time.Time{}
	require.Equal(t, &fleet.MDMCommandResult{
		HostUUID:    orbitHost.UUID,
		CommandUUID: cmdTwoUUID,
		Status:      "200",
		RequestType: "./Device/Vendor/MSFT/DMClient/Provider/DEMO%%20MDM/SignedEntDMID",
		Result:      getCommandFullResult(cmdTwoUUID),
		Payload:     commandTwo.RawCommand,
		Hostname:    orbitHost.Hostname,
	}, getMDMCmdResp.Results[0])

	s.DoJSON("GET", "/api/latest/fleet/commands/results", nil, http.StatusOK, &getMDMCmdResp, "command_uuid", cmdThreeUUID)
	require.Len(t, getMDMCmdResp.Results, 1)
	require.NotZero(t, getMDMCmdResp.Results[0].UpdatedAt)
	getMDMCmdResp.Results[0].UpdatedAt = time.Time{}
	require.Equal(t, &fleet.MDMCommandResult{
		HostUUID:    orbitHost.UUID,
		CommandUUID: cmdThreeUUID,
		Status:      "200",
		RequestType: "./Device/Vendor/MSFT/DMClient/Provider/DEMO%%20MDM/SignedEntDMID",
		Result:      getCommandFullResult(cmdThreeUUID),
		Hostname:    orbitHost.Hostname,
		Payload:     commandThree.RawCommand,
	}, getMDMCmdResp.Results[0])

	s.DoJSON("GET", "/api/latest/fleet/commands/results", nil, http.StatusOK, &getMDMCmdResp, "command_uuid", cmdFourUUID)
	require.Len(t, getMDMCmdResp.Results, 1)
	require.NotZero(t, getMDMCmdResp.Results[0].UpdatedAt)
	getMDMCmdResp.Results[0].UpdatedAt = time.Time{}
	require.Equal(t, &fleet.MDMCommandResult{
		HostUUID:    orbitHost.UUID,
		CommandUUID: cmdFourUUID,
		Status:      "200",
		RequestType: "./Vendor/MSFT/WiFi/Profile/MyNetwork/WlanXml",
		Result:      getCommandFullResult(cmdFourUUID),
		Hostname:    orbitHost.Hostname,
		Payload:     commandFour.RawCommand,
	}, getMDMCmdResp.Results[0])
}

func (s *integrationMDMTestSuite) TestWindowsMDMCommandWithSecret() {
	t := s.T()
	orbitHost, d := createWindowsHostThenEnrollMDM(s.ds, s.server.URL, t)

	secretValue := "abcd1234"
	req := createSecretVariablesRequest{
		SecretVariables: []fleet.SecretVariable{
			{
				Name:  "FLEET_SECRET_DATA",
				Value: secretValue,
			},
		},
	}
	secretResp := createSecretVariablesResponse{}
	s.DoJSON("PUT", "/api/latest/fleet/spec/secret_variables", req, http.StatusOK, &secretResp)

	cmdOneUUID := uuid.New().String()
	commandOne := &fleet.MDMWindowsCommand{
		CommandUUID: cmdOneUUID,
		RawCommand: []byte(fmt.Sprintf(`
                     <Exec>
                       <CmdID>%s</CmdID>
                       <Item>
                         <Target>
                           <LocURI>./Device/Vendor/MSFT/Reboot/RebootNow</LocURI>
                         </Target>
                         <Meta>
                           <Format xmlns="syncml:metinf">null</Format>
                           <Type>text/plain</Type>
                         </Meta>
                         <Data>$FLEET_SECRET_DATA</Data>
                       </Item>
                     </Exec>
		`, cmdOneUUID)),
		TargetLocURI: "./Device/Vendor/MSFT/Reboot/RebootNow",
	}
	err := s.ds.MDMWindowsInsertCommandForHosts(context.Background(), []string{orbitHost.UUID}, commandOne)
	require.NoError(t, err)

	cmds, err := d.StartManagementSession()
	require.NoError(t, err)
	// 2 Status + 1 Exec
	require.Len(t, cmds, 3)
	receivedCmd := cmds[cmdOneUUID]
	require.NotNil(t, receivedCmd)
	require.Equal(t, receivedCmd.Verb, fleet.CmdExec)
	require.Len(t, receivedCmd.Cmd.Items, 1)
	require.EqualValues(t, "./Device/Vendor/MSFT/Reboot/RebootNow", *receivedCmd.Cmd.Items[0].Target)
	assert.EqualValues(t, secretValue, receivedCmd.Cmd.Items[0].Data.Content)

	msgID, err := d.GetCurrentMsgID()
	require.NoError(t, err)

	d.AppendResponse(fleet.SyncMLCmd{
		XMLName: xml.Name{Local: fleet.CmdStatus},
		MsgRef:  &msgID,
		CmdRef:  &cmdOneUUID,
		Cmd:     ptr.String("Exec"),
		Data:    ptr.String("200"),
		Items:   nil,
		CmdID:   fleet.CmdID{Value: uuid.NewString()},
	})
	cmds, err = d.SendResponse()
	require.NoError(t, err)
	// the ack of the message should be the only returned command
	require.Len(t, cmds, 1)

	var getMDMCmdResp getMDMCommandResultsResponse
	s.DoJSON("GET", "/api/latest/fleet/commands/results", nil, http.StatusOK, &getMDMCmdResp, "command_uuid", cmdOneUUID)
	require.Len(t, getMDMCmdResp.Results, 1)
	// The secret value should not be exposed via the regular API.
	assert.NotContains(t, string(getMDMCmdResp.Results[0].Payload), secretValue)
	assert.Contains(t, string(getMDMCmdResp.Results[0].Payload), "$FLEET_SECRET_DATA")
}

func (s *integrationMDMTestSuite) TestWindowsAutomaticEnrollmentCommands() {
	t := s.T()
	ctx := context.Background()

	// define a global enroll secret
	err := s.ds.ApplyEnrollSecrets(ctx, nil, []*fleet.EnrollSecret{{Secret: t.Name()}})
	require.NoError(t, err)

	azureMail := "foo.bar.baz@example.com"
	d := mdmtest.NewTestMDMClientWindowsAutomatic(s.server.URL, azureMail)
	require.NoError(t, d.Enroll())

	checkinAndAck := func(expectFleetdCmds bool) {
		cmds, err := d.StartManagementSession()
		require.NoError(t, err)

		if !expectFleetdCmds {
			// receives only the 2 status commands
			require.Len(t, cmds, 2)
			for _, c := range cmds {
				require.Equal(t, "Status", c.Verb, c)
			}
			return
		}

		// 2 status + 2 commands to install fleetd
		require.Len(t, cmds, 4)
		var fleetdAddCmd, fleetdExecCmd fleet.ProtoCmdOperation
		for _, c := range cmds {
			switch c.Verb {
			case "Add":
				fleetdAddCmd = c
			case "Exec":
				fleetdExecCmd = c
			}
		}
		require.Equal(t, syncml.FleetdWindowsInstallerGUID, fleetdAddCmd.Cmd.GetTargetURI())
		require.Equal(t, syncml.FleetdWindowsInstallerGUID, fleetdExecCmd.Cmd.GetTargetURI())
		require.Len(t, fleetdExecCmd.Cmd.Items, 1)

		var installJob struct {
			Product struct {
				ContentURL string `xml:"Download>ContentURLList>ContentURL"`
				FileHash   string `xml:"Validation>FileHash"`
			} `xml:"Product"`
		}
		err = xml.Unmarshal([]byte(fleetdExecCmd.Cmd.Items[0].Data.Content), &installJob)
		require.NoError(t, err)
		require.Equal(t, s.mockedDownloadFleetdmMeta.MSIURL, installJob.Product.ContentURL)
		require.Equal(t, s.mockedDownloadFleetdmMeta.MSISha256, installJob.Product.FileHash)

		// reply with success for both commands
		msgID, err := d.GetCurrentMsgID()
		require.NoError(t, err)

		d.AppendResponse(fleet.SyncMLCmd{
			XMLName: xml.Name{Local: fleet.CmdStatus},
			MsgRef:  &msgID,
			CmdRef:  &fleetdAddCmd.Cmd.CmdID.Value,
			Cmd:     &fleetdAddCmd.Verb,
			Data:    ptr.String("200"),
			Items:   nil,
			CmdID:   fleet.CmdID{Value: uuid.NewString()},
		})
		d.AppendResponse(fleet.SyncMLCmd{
			XMLName: xml.Name{Local: fleet.CmdStatus},
			MsgRef:  &msgID,
			CmdRef:  &fleetdExecCmd.Cmd.CmdID.Value,
			Cmd:     &fleetdExecCmd.Verb,
			Data:    ptr.String("200"),
			Items:   nil,
			CmdID:   fleet.CmdID{Value: uuid.NewString()},
		})
		cmds, err = d.SendResponse()
		require.NoError(t, err)

		// the ack of the message should be the only returned command
		require.Len(t, cmds, 1)
	}

	// start a management session, will receive the install fleetd commands
	checkinAndAck(true)

	// start a new management session again, Fleetd is not reported as installed
	// so it receives the commands again
	checkinAndAck(true)

	// simulate fleetd installed and enrolled
	host := createOrbitEnrolledHost(t, "windows", "h1", s.ds)
	updated, err := s.ds.UpdateMDMWindowsEnrollmentsHostUUID(ctx, host.UUID, d.DeviceID)
	require.NoError(t, err)
	require.True(t, updated)
	err = s.ds.SetOrUpdateHostOrbitInfo(ctx, host.ID, "1.23", sql.NullString{}, sql.NullBool{})
	require.NoError(t, err)

	// start a new management session again, Fleetd is reported as installed so
	// it does not receive the commands
	checkinAndAck(false)
}

func (s *integrationMDMTestSuite) TestWindowsAzureInitiatedEnrollmentAndMapping() {
	t := s.T()
	ctx := context.Background()

	// define a global enroll secret
	err := s.ds.ApplyEnrollSecrets(ctx, nil, []*fleet.EnrollSecret{{Secret: t.Name()}})
	require.NoError(t, err)

	team, err := s.ds.NewTeam(context.Background(), &fleet.Team{
		Name:        "team1_" + t.Name(),
		Description: "desc team1_" + t.Name(),
	})
	require.NoError(t, err)

	// Enroll another host to ensure the wires don't get crossed somehow
	autopilotUserMail := "swan@example.com"
	autopilotDevice := enrollWindowsHostInMDMViaAutopilot(t, s.ds, s.server.URL, autopilotUserMail)
	require.NoError(t, autopilotDevice.Enroll())

	settingsAppUserMail := "fleetie@example.com"
	settingsAppHost, settingsAppDevice := createWindowsHostThenEnrollMDMViaSettingsApp(s.ds, s.server.URL, settingsAppUserMail, t)
	require.NoError(t, settingsAppDevice.Enroll())

	// Transfer the host to the team. Ensure it doesn't wind up in "No team" at the end
	s.DoJSON("POST", "/api/latest/fleet/hosts/transfer", addHostsToTeamRequest{TeamID: &team.ID, HostIDs: []uint{settingsAppHost.ID}}, http.StatusOK, &addHostsToTeamResponse{})

	checkinAndAck := func(device *mdmtest.TestWindowsMDMClient, expectFleetdCmds bool) {
		cmds, err := device.StartManagementSession()
		require.NoError(t, err)

		if !expectFleetdCmds {
			// receives only the 2 status commands
			require.Len(t, cmds, 2)
			for _, c := range cmds {
				require.Equal(t, "Status", c.Verb, c)
			}
			return
		}

		// Enrollment via settings app or autopilot always results in a fleetd install command, even if already installed,
		// as there's no way to tell at point of MDM enrollment if fleetd is already installed.
		// 2 status + 2 commands to install fleetd
		require.Len(t, cmds, 4)
		var fleetdAddCmd, fleetdExecCmd fleet.ProtoCmdOperation
		for _, c := range cmds {
			switch c.Verb {
			case "Add":
				fleetdAddCmd = c
			case "Exec":
				fleetdExecCmd = c
			}
		}
		require.Equal(t, syncml.FleetdWindowsInstallerGUID, fleetdAddCmd.Cmd.GetTargetURI())
		require.Equal(t, syncml.FleetdWindowsInstallerGUID, fleetdExecCmd.Cmd.GetTargetURI())
		require.Len(t, fleetdExecCmd.Cmd.Items, 1)

		var installJob struct {
			Product struct {
				ContentURL  string `xml:"Download>ContentURLList>ContentURL"`
				FileHash    string `xml:"Validation>FileHash"`
				CommandLine string `xml:"Enforcement>CommandLine"`
			} `xml:"Product"`
		}
		err = xml.Unmarshal([]byte(fleetdExecCmd.Cmd.Items[0].Data.Content), &installJob)
		require.NoError(t, err)
		require.Equal(t, s.mockedDownloadFleetdmMeta.MSIURL, installJob.Product.ContentURL)
		require.Equal(t, s.mockedDownloadFleetdmMeta.MSISha256, installJob.Product.FileHash)
		// Test name is our simulated enroll secret
		require.Contains(t, installJob.Product.CommandLine, "FLEET_SECRET=\""+t.Name()+"\"")

		// reply with success for both commands
		msgID, err := device.GetCurrentMsgID()
		require.NoError(t, err)

		device.AppendResponse(fleet.SyncMLCmd{
			XMLName: xml.Name{Local: fleet.CmdStatus},
			MsgRef:  &msgID,
			CmdRef:  &fleetdAddCmd.Cmd.CmdID.Value,
			Cmd:     &fleetdAddCmd.Verb,
			Data:    ptr.String("200"),
			Items:   nil,
			CmdID:   fleet.CmdID{Value: uuid.NewString()},
		})
		device.AppendResponse(fleet.SyncMLCmd{
			XMLName: xml.Name{Local: fleet.CmdStatus},
			MsgRef:  &msgID,
			CmdRef:  &fleetdExecCmd.Cmd.CmdID.Value,
			Cmd:     &fleetdExecCmd.Verb,
			Data:    ptr.String("200"),
			Items:   nil,
			CmdID:   fleet.CmdID{Value: uuid.NewString()},
		})
		cmds, err = device.SendResponse()
		require.NoError(t, err)

		// the ack of the message should be the only returned command
		require.Len(t, cmds, 1)
	}

	// start a management session, will receive the install fleetd commands
	checkinAndAck(settingsAppDevice, true)

	// start a new management session again, Fleetd is not reported as installed
	// so it receives the commands again.
	// Note: This doesn't mean on a real host the actual install will run twice, review
	// Microsoft MDM code to see how this is done so that it only installs once
	checkinAndAck(settingsAppDevice, true)

	// Start a management session for the autopilot device then create the host
	checkinAndAck(autopilotDevice, true)

	autopilotHost := createOrbitEnrolledHost(t, "windows", uuid.NewString(), s.ds)

	// A new management session should still send the install commands for now
	checkinAndAck(autopilotDevice, true)

	// Simulate fleetd fetching queries and reporting back the device ID via MDM queries
	// for the "settings app" device
	s.simulateMDMWindowsQueries(t, settingsAppHost, settingsAppDevice)

	// The MDM Enrollment should now be linked to the host
	mdmEnrollment, err := s.ds.MDMWindowsGetEnrolledDeviceWithHostUUID(ctx, settingsAppHost.UUID)
	require.NoError(t, err)
	require.Equal(t, settingsAppDevice.DeviceID, mdmEnrollment.MDMDeviceID)

	// start a new management session again, Fleetd is reported as installed so
	// it does not receive the commands
	checkinAndAck(settingsAppDevice, false)

	// But the autopilot device still receives commands...
	checkinAndAck(autopilotDevice, true)

	var hostResp getHostResponse
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", settingsAppHost.ID), nil, http.StatusOK, &hostResp)
	// Host was not transferred
	require.NotNil(t, hostResp.Host.TeamID)
	require.Equal(t, team.ID, *hostResp.Host.TeamID)

	// Host enrolled via settings app and is marked as enrolled manually
	require.NotNil(t, hostResp.Host.MDM.EnrollmentStatus)
	require.Equal(t, "On (manual)", *hostResp.Host.MDM.EnrollmentStatus)

	// Host has the end user mapped correctly
	require.Len(t, hostResp.Host.EndUsers, 1)
	require.Equal(t, settingsAppUserMail, hostResp.Host.EndUsers[0].IdpUserName)

	// Autopilot host checks in, gets queries, reports back
	s.simulateMDMWindowsQueries(t, autopilotHost, autopilotDevice)

	mdmEnrollment, err = s.ds.MDMWindowsGetEnrolledDeviceWithHostUUID(ctx, autopilotHost.UUID)
	require.NoError(t, err)
	require.Equal(t, autopilotDevice.DeviceID, mdmEnrollment.MDMDeviceID)

	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", autopilotHost.ID), nil, http.StatusOK, &hostResp)

	// Host enrolled via Autopilot and is marked as enrolled automatically
	require.NotNil(t, hostResp.Host.MDM.EnrollmentStatus)
	require.Equal(t, "On (automatic)", *hostResp.Host.MDM.EnrollmentStatus)

	// Host has the end user mapped correctly
	require.Len(t, hostResp.Host.EndUsers, 1)
	require.Equal(t, autopilotUserMail, hostResp.Host.EndUsers[0].IdpUserName)

	// Re-run the queries
	s.simulateMDMWindowsQueries(t, autopilotHost, autopilotDevice)
	s.simulateMDMWindowsQueries(t, settingsAppHost, settingsAppDevice)

	// Just as a final sanity check the autopilot host is still marked as enrolled automatically
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", autopilotHost.ID), nil, http.StatusOK, &hostResp)

	// Host enrolled via Autopilot and is marked as enrolled automatically
	require.NotNil(t, hostResp.Host.MDM.EnrollmentStatus)
	require.Equal(t, "On (automatic)", *hostResp.Host.MDM.EnrollmentStatus)

	// And the Settings app host manually
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", settingsAppHost.ID), nil, http.StatusOK, &hostResp)

	// Host enrolled via settings app and is marked as enrolled manually
	require.NotNil(t, hostResp.Host.MDM.EnrollmentStatus)
	require.Equal(t, "On (manual)", *hostResp.Host.MDM.EnrollmentStatus)
}

func (s *integrationMDMTestSuite) TestValidManagementUnenrollRequest() {
	t := s.T()

	// Target Endpoint URL for the management endpoint
	targetEndpointURL := microsoft_mdm.MDE2ManagementPath

	// Target DeviceID to use
	deviceID := "DB257C3A08778F4FB61E2749066C1F27"

	// Inserting new device
	enrolledDevice := &fleet.MDMWindowsEnrolledDevice{
		MDMDeviceID:            deviceID,
		MDMHardwareID:          uuid.New().String() + uuid.New().String(),
		MDMDeviceState:         uuid.New().String(),
		MDMDeviceType:          "CIMClient_Windows",
		MDMDeviceName:          "DESKTOP-1C3ARC1",
		MDMEnrollType:          "ProgrammaticEnrollment",
		MDMEnrollUserID:        "upn@domain.com",
		MDMEnrollProtoVersion:  "5.0",
		MDMEnrollClientVersion: "10.0.19045.2965",
		MDMNotInOOBE:           false,
	}

	err := s.ds.MDMWindowsInsertEnrolledDevice(context.Background(), enrolledDevice)
	require.NoError(t, err)

	// Checking if device was enrolled
	_, err = s.ds.MDMWindowsGetEnrolledDeviceWithDeviceID(context.Background(), deviceID)
	require.NoError(t, err)

	// Preparing the SyncML unenroll request
	requestBytes, err := s.newSyncMLUnenrollMsg(deviceID, targetEndpointURL)
	require.NoError(t, err)

	resp := s.DoRaw("POST", targetEndpointURL, requestBytes, http.StatusOK)

	// Checking that Command error code was updated

	// Checking response headers
	require.Contains(t, resp.Header["Content-Type"], syncml.SyncMLContentType)

	// Read response data
	resBytes, err := io.ReadAll(resp.Body)
	require.NoError(t, err)

	// Checking if response can be unmarshalled to an golang type
	var xmlType interface{}
	err = xml.Unmarshal(resBytes, &xmlType)
	require.NoError(t, err)

	// Checking if device was unenrolled
	_, err = s.ds.MDMWindowsGetEnrolledDeviceWithDeviceID(context.Background(), deviceID)
	require.True(t, fleet.IsNotFound(err))
}

func (s *integrationMDMTestSuite) TestRunMDMCommands() {
	t := s.T()

	// create a Windows host enrolled in MDM
	enrolledWindows, _ := createWindowsHostThenEnrollMDM(s.ds, s.server.URL, t)

	// create an unenrolled Windows host
	unenrolledWindows := createOrbitEnrolledHost(t, "windows", "h2", s.ds)

	// create an enrolled and unenrolled macOS host
	enrolledMac, _ := createHostThenEnrollMDM(s.ds, s.server.URL, t)
	unenrolledMac := createOrbitEnrolledHost(t, "darwin", "h4", s.ds)

	macRawCmd := `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Command</key>
    <dict>
        <key>RequestType</key>
        <string>ShutDownDevice</string>
    </dict>
    <key>CommandUUID</key>
    <string>0001_ShutDownDevice</string>
</dict>
</plist>`

	winRawCmd := `
<Exec>
	<CmdID>11</CmdID>
	<Item>
		<Target>
			<LocURI>./SetValues</LocURI>
		</Target>
		<Meta>
			<Format xmlns="syncml:metinf">chr</Format>
			<Type xmlns="syncml:metinf">text/plain</Type>
		</Meta>
		<Data>NamedValuesList=MinPasswordLength,8;</Data>
	</Item>
</Exec>
`

	var runResp runMDMCommandResponse

	// no host provided
	s.DoJSON("POST", "/api/latest/fleet/commands/run", &runMDMCommandRequest{
		Command: base64.StdEncoding.EncodeToString([]byte(macRawCmd)),
	}, http.StatusNotFound, &runResp)

	// mix of mdm and non-mdm hosts
	s.DoJSON("POST", "/api/latest/fleet/commands/run", &runMDMCommandRequest{
		Command:   base64.StdEncoding.EncodeToString([]byte(macRawCmd)),
		HostUUIDs: []string{enrolledMac.UUID, unenrolledMac.UUID},
	}, http.StatusPreconditionFailed, &runResp)
	s.DoJSON("POST", "/api/latest/fleet/commands/run", &runMDMCommandRequest{
		Command:   base64.StdEncoding.EncodeToString([]byte(winRawCmd)),
		HostUUIDs: []string{enrolledWindows.UUID, unenrolledWindows.UUID},
	}, http.StatusPreconditionFailed, &runResp)

	// mix of windows and macos hosts
	s.DoJSON("POST", "/api/latest/fleet/commands/run", &runMDMCommandRequest{
		Command:   base64.StdEncoding.EncodeToString([]byte(macRawCmd)),
		HostUUIDs: []string{enrolledMac.UUID, enrolledWindows.UUID},
	}, http.StatusUnprocessableEntity, &runResp)

	// windows only, invalid command
	res := s.Do("POST", "/api/latest/fleet/commands/run", &runMDMCommandRequest{
		Command:   base64.StdEncoding.EncodeToString([]byte(macRawCmd)),
		HostUUIDs: []string{enrolledWindows.UUID},
	}, http.StatusUnprocessableEntity)
	errMsg := extractServerErrorText(res.Body)
	require.Contains(t, errMsg, "You can run only <Exec> command type")

	// macOS only, invalid command
	res = s.Do("POST", "/api/latest/fleet/commands/run", &runMDMCommandRequest{
		Command:   base64.StdEncoding.EncodeToString([]byte(winRawCmd)),
		HostUUIDs: []string{enrolledMac.UUID},
	}, http.StatusUnsupportedMediaType)
	errMsg = extractServerErrorText(res.Body)
	require.Contains(t, errMsg, "unable to decode plist command")

	// valid windows
	runResp = runMDMCommandResponse{}
	s.DoJSON("POST", "/api/latest/fleet/commands/run", &runMDMCommandRequest{
		Command:   base64.StdEncoding.EncodeToString([]byte(winRawCmd)),
		HostUUIDs: []string{enrolledWindows.UUID},
	}, http.StatusOK, &runResp)
	require.NotEmpty(t, runResp.CommandUUID)
	require.Equal(t, "windows", runResp.Platform)
	require.Equal(t, "./SetValues", runResp.RequestType)

	// valid macOS
	runResp = runMDMCommandResponse{}
	s.DoJSON("POST", "/api/latest/fleet/commands/run", &runMDMCommandRequest{
		Command:   base64.StdEncoding.EncodeToString([]byte(macRawCmd)),
		HostUUIDs: []string{enrolledMac.UUID},
	}, http.StatusOK, &runResp)
	require.NotEmpty(t, runResp.CommandUUID)
	require.Equal(t, "darwin", runResp.Platform)
	require.Equal(t, "ShutDownDevice", runResp.RequestType)
}

func (s *integrationMDMTestSuite) TestUpdateMDMWindowsEnrollmentsHostUUID() {
	ctx := context.Background()
	t := s.T()

	// simulate device that is MDM enrolled before fleetd is installed
	d := fleet.MDMWindowsEnrolledDevice{
		MDMDeviceID:            "test-device-id",
		MDMHardwareID:          "test-hardware-id",
		MDMDeviceState:         "ds",
		MDMDeviceType:          "dt",
		MDMDeviceName:          "dn",
		MDMEnrollType:          "et",
		MDMEnrollUserID:        "euid",
		MDMEnrollProtoVersion:  "epv",
		MDMEnrollClientVersion: "ecv",
		MDMNotInOOBE:           false,
		HostUUID:               "", // empty host uuid when created
	}
	require.NoError(t, s.ds.MDMWindowsInsertEnrolledDevice(ctx, &d))

	gotDevice, err := s.ds.MDMWindowsGetEnrolledDeviceWithDeviceID(ctx, d.MDMDeviceID)
	require.NoError(t, err)
	require.Empty(t, gotDevice.HostUUID)

	// create an enroll secret
	secret := uuid.New().String()
	var applyResp applyEnrollSecretSpecResponse
	s.DoJSON("POST", "/api/latest/fleet/spec/enroll_secret", applyEnrollSecretSpecRequest{
		Spec: &fleet.EnrollSecretSpec{
			Secrets: []*fleet.EnrollSecret{{Secret: secret}},
		},
	}, http.StatusOK, &applyResp)

	// simulate fleetd installed and enrolled
	var resp EnrollOrbitResponse
	hostUUID := uuid.New().String()
	hostSerial := "test-host-serial"
	s.DoJSON("POST", "/api/fleet/orbit/enroll", contract.EnrollOrbitRequest{
		EnrollSecret:   secret,
		HardwareUUID:   hostUUID,
		HardwareSerial: hostSerial,
		Platform:       "windows",
	}, http.StatusOK, &resp)
	require.NotEmpty(t, resp.OrbitNodeKey)

	gotDevice, err = s.ds.MDMWindowsGetEnrolledDeviceWithDeviceID(ctx, d.MDMDeviceID)
	require.NoError(t, err)
	require.Empty(t, gotDevice.HostUUID)

	// simulate first report osquery host details
	updated, err := s.ds.UpdateMDMWindowsEnrollmentsHostUUID(ctx, hostUUID, d.MDMDeviceID)
	require.NoError(t, err)
	require.True(t, updated)

	// check that the host uuid was updated
	gotDevice, err = s.ds.MDMWindowsGetEnrolledDeviceWithDeviceID(ctx, d.MDMDeviceID)
	require.NoError(t, err)
	require.NotEmpty(t, gotDevice.HostUUID)
	require.Equal(t, hostUUID, gotDevice.HostUUID)
}

func (s *integrationMDMTestSuite) TestBitLockerEnforcementNotifications() {
	t := s.T()
	ctx := context.Background()
	windowsHost := createOrbitEnrolledHost(t, "windows", t.Name(), s.ds)

	checkNotification := func(want bool) {
		resp := orbitGetConfigResponse{}
		s.DoJSON("POST", "/api/fleet/orbit/config", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *windowsHost.OrbitNodeKey)), http.StatusOK, &resp)
		require.Equal(t, want, resp.Notifications.EnforceBitLockerEncryption)
	}

	// notification is false by default
	checkNotification(false)

	// enroll the host into Fleet MDM
	encodedBinToken, err := fleet.GetEncodedBinarySecurityToken(fleet.WindowsMDMProgrammaticEnrollmentType, *windowsHost.OrbitNodeKey)
	require.NoError(t, err)
	requestBytes, err := s.newSecurityTokenMsg(encodedBinToken, true, false)
	require.NoError(t, err)
	s.DoRaw("POST", microsoft_mdm.MDE2EnrollPath, requestBytes, http.StatusOK)

	// simulate osquery checking in and updating this info
	// TODO: should we automatically fill these fields on MDM enrollment?
	require.NoError(t, s.ds.SetOrUpdateMDMData(context.Background(), windowsHost.ID, false, true, "https://example.com", true, fleet.WellKnownMDMFleet, "", false))

	// notification is still false
	checkNotification(false)

	// configure disk encryption for the global team
	acResp := appConfigResponse{}
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{ "mdm": { "macos_settings": { "enable_disk_encryption": true } } }`), http.StatusOK, &acResp)
	assert.True(t, acResp.MDM.EnableDiskEncryption.Value)

	// host still doesn't get the notification because we don't have disk
	// encryption information yet.
	checkNotification(false)

	// host has disk encryption off, gets the notification
	require.NoError(t, s.ds.SetOrUpdateHostDisksEncryption(context.Background(), windowsHost.ID, false))
	checkNotification(true)

	// host has disk encryption on, we don't have disk encryption info. Gets the notification
	require.NoError(t, s.ds.SetOrUpdateHostDisksEncryption(context.Background(), windowsHost.ID, true))
	checkNotification(true)

	// host has disk encryption on, we don't know if the key is decriptable. Gets the notification
	_, err = s.ds.SetOrUpdateHostDiskEncryptionKey(ctx, windowsHost, "test-key", "", nil)
	require.NoError(t, err)
	checkNotification(true)

	// host has disk encryption on, the key is not decryptable by fleet. Gets the notification
	err = s.ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{windowsHost.ID}, false, time.Now())
	require.NoError(t, err)
	checkNotification(true)

	// host has disk encryption on, the disk was encrypted by fleet. Doesn't get the notification
	err = s.ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{windowsHost.ID}, true, time.Now())
	require.NoError(t, err)
	checkNotification(false)

	// create a new team
	tm, err := s.ds.NewTeam(context.Background(), &fleet.Team{
		Name:        t.Name(),
		Description: "desc",
	})
	require.NoError(t, err)
	// add the host to the team
	err = s.ds.AddHostsToTeam(context.Background(), fleet.NewAddHostsToTeamParams(&tm.ID, []uint{windowsHost.ID}))
	require.NoError(t, err)

	// notification is false now since the team doesn't have disk encryption enabled
	checkNotification(false)

	// enable disk encryption on the team
	teamSpecs := applyTeamSpecsRequest{Specs: []*fleet.TeamSpec{{
		Name: tm.Name,
		MDM: fleet.TeamSpecMDM{
			EnableDiskEncryption: optjson.SetBool(true),
		},
	}}}
	s.Do("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK)

	// host gets the notification
	checkNotification(true)

	// host has disk encryption off, gets the notification
	require.NoError(t, s.ds.SetOrUpdateHostDisksEncryption(context.Background(), windowsHost.ID, false))
	checkNotification(true)

	// host has disk encryption on, we don't have disk encryption info. Gets the notification
	require.NoError(t, s.ds.SetOrUpdateHostDisksEncryption(context.Background(), windowsHost.ID, true))
	checkNotification(true)

	// host has disk encryption on, we don't know if the key is decriptable. Gets the notification
	_, err = s.ds.SetOrUpdateHostDiskEncryptionKey(ctx, windowsHost, "test-key", "", nil)
	require.NoError(t, err)
	checkNotification(true)

	// host has disk encryption on, the key is not decryptable by fleet. Gets the notification
	err = s.ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{windowsHost.ID}, false, time.Now())
	require.NoError(t, err)
	checkNotification(true)

	// host has disk encryption on, the disk was encrypted by fleet. Doesn't get the notification
	err = s.ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{windowsHost.ID}, true, time.Now())
	require.NoError(t, err)
	checkNotification(false)
}

func (s *integrationMDMTestSuite) TestHostDiskEncryptionKey() {
	t := s.T()
	ctx := context.Background()

	host := createOrbitEnrolledHost(t, "windows", "h1", s.ds)

	// turn on disk encryption for the global team
	acResp := appConfigResponse{}
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{ "mdm": { "enable_disk_encryption": true } }`), http.StatusOK, &acResp)
	assert.True(t, acResp.AppConfig.MDM.EnableDiskEncryption.Value)

	// try to call the endpoint while the host is not MDM-enrolled
	res := s.Do("POST", "/api/fleet/orbit/disk_encryption_key", orbitPostDiskEncryptionKeyRequest{
		OrbitNodeKey:  *host.OrbitNodeKey,
		EncryptionKey: []byte("WILL-FAIL"),
	}, http.StatusBadRequest)
	msg := extractServerErrorText(res.Body)
	require.Contains(t, msg, "host is not enrolled with fleet")

	// enroll it in fleet
	mdmDevice := mdmtest.NewTestMDMClientWindowsProgramatic(s.server.URL, *host.OrbitNodeKey)
	err := mdmDevice.Enroll()
	require.NoError(t, err)
	err = s.ds.SetOrUpdateMDMData(ctx, host.ID, false, true, s.server.URL, false, fleet.WellKnownMDMFleet, "", false)
	require.NoError(t, err)

	// set its encryption key
	s.Do("POST", "/api/fleet/orbit/disk_encryption_key", orbitPostDiskEncryptionKeyRequest{
		OrbitNodeKey:  *host.OrbitNodeKey,
		EncryptionKey: []byte("ABC"),
	}, http.StatusNoContent)

	hdek, err := s.ds.GetHostDiskEncryptionKey(ctx, host.ID)
	require.NoError(t, err)
	require.NotNil(t, hdek.Decryptable)
	require.True(t, *hdek.Decryptable)

	// check a new activity entry is created ...
	activities, _, err := s.ds.ListActivities(ctx, fleet.ListActivitiesOptions{})
	require.NoError(t, err)
	escrowKeyActivity := fleet.ActivityTypeEscrowedDiskEncryptionKey{}
	var seenEscrowKeyActivityID uint
	for _, activity := range activities {
		if activity.Type == escrowKeyActivity.ActivityName() {
			err := json.Unmarshal(*activity.Details, &escrowKeyActivity)
			require.NoError(t, err)
			require.True(t, activity.FleetInitiated)

			seenEscrowKeyActivityID = activity.ID
		}
	}
	require.NotZero(t, seenEscrowKeyActivityID)
	require.Equal(t, escrowKeyActivity.HostID, host.ID)
	require.Equal(t, escrowKeyActivity.HostDisplayName, host.DisplayName())

	// mark it as non-server
	err = s.ds.SetOrUpdateMDMData(ctx, host.ID, false, true, s.server.URL, false, fleet.WellKnownMDMFleet, "", false)
	require.NoError(t, err)

	var hostResp getHostResponse
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &hostResp)
	require.Nil(t, hostResp.Host.DiskEncryptionEnabled) // the disk encryption status of the host is not set by the orbit request
	require.NotNil(t, hostResp.Host.MDM.OSSettings)
	require.NotNil(t, hostResp.Host.MDM.OSSettings.DiskEncryption.Status)
	require.Equal(t, fleet.DiskEncryptionEnforcing, *hostResp.Host.MDM.OSSettings.DiskEncryption.Status) // still pending because disk encryption status is not set
	require.Equal(t, "", hostResp.Host.MDM.OSSettings.DiskEncryption.Detail)

	// the key is encrypted the same way as the macOS keys (except with the WSTEP
	// certificate), so it can be decrypted using the same decryption function.
	wstepCert, _, _, err := s.fleetCfg.MDM.MicrosoftWSTEP()
	require.NoError(t, err)
	decrypted, err := servermdm.DecryptBase64CMS(hdek.Base64Encrypted, wstepCert.Leaf, wstepCert.PrivateKey)
	require.NoError(t, err)
	require.Equal(t, "ABC", string(decrypted))

	// set it with a client error
	s.Do("POST", "/api/fleet/orbit/disk_encryption_key", orbitPostDiskEncryptionKeyRequest{
		OrbitNodeKey: *host.OrbitNodeKey,
		ClientError:  "fail",
	}, http.StatusNoContent)

	hdek, err = s.ds.GetHostDiskEncryptionKey(ctx, host.ID)
	require.NoError(t, err)
	require.Nil(t, hdek.Decryptable)
	require.Empty(t, hdek.Base64Encrypted)

	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &hostResp)
	require.Nil(t, hostResp.Host.DiskEncryptionEnabled) // the disk encryption status of the host is not set by the orbit request
	require.NotNil(t, hostResp.Host.MDM.OSSettings)
	require.NotNil(t, hostResp.Host.MDM.OSSettings.DiskEncryption.Status)
	require.Equal(t, fleet.DiskEncryptionFailed, *hostResp.Host.MDM.OSSettings.DiskEncryption.Status)
	require.Equal(t, "fail", hostResp.Host.MDM.OSSettings.DiskEncryption.Detail)

	// set a different key
	s.Do("POST", "/api/fleet/orbit/disk_encryption_key", orbitPostDiskEncryptionKeyRequest{
		OrbitNodeKey:  *host.OrbitNodeKey,
		EncryptionKey: []byte("DEF"),
	}, http.StatusNoContent)

	hdek, err = s.ds.GetHostDiskEncryptionKey(ctx, host.ID)
	require.NoError(t, err)
	require.NotNil(t, hdek.Decryptable)
	require.True(t, *hdek.Decryptable)

	// escrowing a new encryption key should create a new acitivy entry
	activities, _, err = s.ds.ListActivities(ctx, fleet.ListActivitiesOptions{})
	require.NoError(t, err)
	escrowKeyActivity = fleet.ActivityTypeEscrowedDiskEncryptionKey{}
	for _, activity := range activities {
		if activity.Type == escrowKeyActivity.ActivityName() && activity.ID != seenEscrowKeyActivityID {
			err := json.Unmarshal(*activity.Details, &escrowKeyActivity)
			require.NoError(t, err)
			require.True(t, activity.FleetInitiated)

			seenEscrowKeyActivityID = activity.ID
		}
	}
	require.Equal(t, escrowKeyActivity.HostID, host.ID)
	require.Equal(t, escrowKeyActivity.HostDisplayName, host.DisplayName())

	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &hostResp)
	require.Nil(t, hostResp.Host.DiskEncryptionEnabled) // the disk encryption status of the host is not set by the orbit request
	require.NotNil(t, hostResp.Host.MDM.OSSettings)
	require.NotNil(t, hostResp.Host.MDM.OSSettings.DiskEncryption.Status)
	require.Equal(t, fleet.DiskEncryptionEnforcing, *hostResp.Host.MDM.OSSettings.DiskEncryption.Status) // still pending because disk encryption status is not set
	require.Equal(t, "", hostResp.Host.MDM.OSSettings.DiskEncryption.Detail)

	decrypted, err = servermdm.DecryptBase64CMS(hdek.Base64Encrypted, wstepCert.Leaf, wstepCert.PrivateKey)
	require.NoError(t, err)
	require.Equal(t, "DEF", string(decrypted))

	// report host disks as encrypted
	err = s.ds.SetOrUpdateHostDisksEncryption(ctx, host.ID, true)
	require.NoError(t, err)

	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &hostResp)
	require.True(t, *hostResp.Host.DiskEncryptionEnabled)
	require.NotNil(t, hostResp.Host.MDM.OSSettings)
	require.NotNil(t, hostResp.Host.MDM.OSSettings.DiskEncryption.Status)
	require.Equal(t, fleet.DiskEncryptionVerified, *hostResp.Host.MDM.OSSettings.DiskEncryption.Status)
	require.Equal(t, "", hostResp.Host.MDM.OSSettings.DiskEncryption.Detail)
}

// ///////////////////////////////////////////////////////////////////////////
// Common MDM config test

func (s *integrationMDMTestSuite) TestMDMEnabledAndConfigured() {
	t := s.T()
	ctx := context.Background()

	appConfig, err := s.ds.AppConfig(ctx)
	originalCopy := appConfig.Copy()
	require.NoError(t, err)

	t.Cleanup(func() {
		require.NoError(t, s.ds.SaveAppConfig(ctx, originalCopy))
	})

	checkAppConfig := func(t *testing.T, mdmEnabled, winEnabled bool) appConfigResponse {
		acResp := appConfigResponse{}
		s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp)
		require.True(t, acResp.AppConfig.MDM.AppleBMEnabledAndConfigured)
		require.Equal(t, mdmEnabled, acResp.AppConfig.MDM.EnabledAndConfigured)
		require.Equal(t, winEnabled, acResp.AppConfig.MDM.WindowsEnabledAndConfigured)
		return acResp
	}

	compareMacOSSetupValues := (func(t *testing.T, got fleet.MacOSSetup, want fleet.MacOSSetup) {
		require.Equal(t, want.BootstrapPackage.Value, got.BootstrapPackage.Value)
		require.Equal(t, want.MacOSSetupAssistant.Value, got.MacOSSetupAssistant.Value)
		require.Equal(t, want.EnableEndUserAuthentication, got.EnableEndUserAuthentication)
	})

	insertBootstrapPackageAndSetupAssistant := func(t *testing.T, teamID *uint) {
		var tmID uint
		if teamID != nil {
			tmID = *teamID
		}

		// cleanup any residual bootstrap package
		_ = s.ds.DeleteMDMAppleBootstrapPackage(ctx, tmID)

		// add new bootstrap package
		require.NoError(t, s.ds.InsertMDMAppleBootstrapPackage(ctx, &fleet.MDMAppleBootstrapPackage{
			TeamID: tmID,
			Name:   "foo",
			Token:  uuid.New().String(),
			Bytes:  []byte("foo"),
			Sha256: []byte("foo-sha256"),
		}, nil))

		// add new setup assistant
		_, err := s.ds.SetOrUpdateMDMAppleSetupAssistant(ctx, &fleet.MDMAppleSetupAssistant{
			TeamID:  teamID,
			Name:    "bar",
			Profile: []byte("{}"),
		})
		require.NoError(t, err)
	}

	// TODO: Some global MDM config settings don't have MDMEnabledAndConfigured or
	// WindowsMDMEnabledAndConfigured validations currently. Either add validations
	// and test them or test absence of validation.
	t.Run("apply app config spec", func(t *testing.T) {
		t.Run("disk encryption", func(t *testing.T) {
			t.Cleanup(func() {
				require.NoError(t, s.ds.SaveAppConfig(ctx, appConfig))
			})

			acResp := checkAppConfig(t, true, true)
			require.False(t, acResp.AppConfig.MDM.EnableDiskEncryption.Value) // disabled by default

			// initialize our test app config
			ac := appConfig.Copy()
			ac.AgentOptions = nil

			// enable disk encryption
			ac.MDM.EnableDiskEncryption = optjson.SetBool(true)
			s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusOK, &acResp)
			acResp = checkAppConfig(t, true, true)                           // both mac and windows mdm enabled
			require.True(t, acResp.AppConfig.MDM.EnableDiskEncryption.Value) // enabled

			// directly set MDM.EnabledAndConfigured to false
			ac.MDM.EnabledAndConfigured = false
			require.NoError(t, s.ds.SaveAppConfig(ctx, ac))
			acResp = checkAppConfig(t, false, true)                          // only windows mdm enabled
			require.True(t, acResp.AppConfig.MDM.EnableDiskEncryption.Value) // disabling mdm doesn't change disk encryption

			// making an unrelated change should not cause validation error
			ac.OrgInfo.OrgName = "f1337"
			s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusOK, &acResp)
			acResp = checkAppConfig(t, false, true)                          // only windows mdm enabled
			require.True(t, acResp.AppConfig.MDM.EnableDiskEncryption.Value) // no change
			require.Equal(t, "f1337", acResp.AppConfig.OrgInfo.OrgName)

			// disabling disk encryption doesn't cause validation error
			ac.MDM.EnableDiskEncryption = optjson.SetBool(false)
			s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusOK, &acResp)
			acResp = checkAppConfig(t, false, true)                           // only windows mdm enabled
			require.False(t, acResp.AppConfig.MDM.EnableDiskEncryption.Value) // disabled
			require.Equal(t, "f1337", acResp.AppConfig.OrgInfo.OrgName)

			// enabling disk encryption doesn't cause validation error
			ac.MDM.EnableDiskEncryption = optjson.SetBool(true)
			s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusOK, &acResp)
			s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp)
			acResp = checkAppConfig(t, false, true)                          // only windows mdm enabled
			require.True(t, acResp.AppConfig.MDM.EnableDiskEncryption.Value) // enabled

			// directly set MDM.WindowsEnabledAndConfigured to false
			ac.MDM.WindowsEnabledAndConfigured = false
			require.NoError(t, s.ds.SaveAppConfig(ctx, ac))
			acResp = checkAppConfig(t, false, false)                         // both mac and windows mdm disabled
			require.True(t, acResp.AppConfig.MDM.EnableDiskEncryption.Value) // disabling mdm doesn't change disk encryption

			// disabling disk encryption doesn't cause validation error
			ac.MDM.EnableDiskEncryption = optjson.SetBool(false)
			s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusOK, &acResp)
			acResp = checkAppConfig(t, false, false)                          // no MDM enabled
			require.False(t, acResp.AppConfig.MDM.EnableDiskEncryption.Value) // disabled
			require.Equal(t, "f1337", acResp.AppConfig.OrgInfo.OrgName)

			// enabling disk encryption doesn't cause validation error
			ac.MDM.EnableDiskEncryption = optjson.SetBool(true)
			s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusOK, &acResp)
			s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp)
			acResp = checkAppConfig(t, false, false)                         // no MDM enabled
			require.True(t, acResp.AppConfig.MDM.EnableDiskEncryption.Value) // enabled

			// changing unrelated config doesn't cause validation error
			ac.OrgInfo.OrgName = "f1338"
			s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusOK, &acResp)
			acResp = checkAppConfig(t, false, false)                         // both mac and windows mdm disabled
			require.True(t, acResp.AppConfig.MDM.EnableDiskEncryption.Value) // no change
			require.Equal(t, "f1338", acResp.AppConfig.OrgInfo.OrgName)
		})

		t.Run("macos setup", func(t *testing.T) {
			t.Cleanup(func() {
				require.NoError(t, s.ds.SaveAppConfig(ctx, appConfig))
			})

			acResp := checkAppConfig(t, true, true)
			compareMacOSSetupValues(t, fleet.MacOSSetup{}, acResp.AppConfig.MDM.MacOSSetup) // disabled by default

			// initialize our test app config
			ac := appConfig.Copy()
			ac.AgentOptions = nil
			ac.MDM.EndUserAuthentication = fleet.MDMEndUserAuthentication{
				SSOProviderSettings: fleet.SSOProviderSettings{
					EntityID:    "sso-provider",
					IDPName:     "sso-provider",
					MetadataURL: "https://sso-provider.example.com/metadata",
				},
			}

			// add db records for bootstrap package and setup assistant
			insertBootstrapPackageAndSetupAssistant(t, nil)

			// enable MacOSSetup options
			ac.MDM.MacOSSetup = fleet.MacOSSetup{
				BootstrapPackage:            optjson.SetString("foo"),
				EnableEndUserAuthentication: true,
				MacOSSetupAssistant:         optjson.SetString("bar"),
			}
			s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusOK, &acResp)
			acResp = checkAppConfig(t, true, true)                               // both mac and windows mdm enabled
			compareMacOSSetupValues(t, acResp.MDM.MacOSSetup, ac.MDM.MacOSSetup) // applied

			// directly set MDM.EnabledAndConfigured to false
			ac.MDM.EnabledAndConfigured = false
			require.NoError(t, s.ds.SaveAppConfig(ctx, ac))
			acResp = checkAppConfig(t, false, true)                              // only windows mdm enabled
			compareMacOSSetupValues(t, acResp.MDM.MacOSSetup, ac.MDM.MacOSSetup) // still applied

			// making an unrelated change should not cause validation error
			ac.OrgInfo.OrgName = "f1337"
			s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusOK, &acResp)
			acResp = checkAppConfig(t, false, true)                              // only windows mdm enabled
			compareMacOSSetupValues(t, acResp.MDM.MacOSSetup, ac.MDM.MacOSSetup) // still applied
			require.Equal(t, "f1337", acResp.AppConfig.OrgInfo.OrgName)

			// disabling doesn't cause validation error
			ac.MDM.MacOSSetup = fleet.MacOSSetup{
				BootstrapPackage:            optjson.SetString(""),
				EnableEndUserAuthentication: false,
				MacOSSetupAssistant:         optjson.SetString(""),
			}
			s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusOK, &acResp)
			acResp = checkAppConfig(t, false, true)                              // only windows mdm enabled
			compareMacOSSetupValues(t, acResp.MDM.MacOSSetup, ac.MDM.MacOSSetup) // applied
			require.Equal(t, "f1337", acResp.AppConfig.OrgInfo.OrgName)

			// bootstrap package and setup assistant were removed so reinsert records for next test
			insertBootstrapPackageAndSetupAssistant(t, nil)

			// enable MacOSSetup options fails because only Windows is enabled.
			ac.MDM.MacOSSetup = fleet.MacOSSetup{
				BootstrapPackage:            optjson.SetString("foo"),
				EnableEndUserAuthentication: true,
				MacOSSetupAssistant:         optjson.SetString("bar"),
			}
			s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusUnprocessableEntity, &acResp)
			acResp = checkAppConfig(t, false, true) // only windows enabled

			// directly set MDM.EnabledAndConfigured to true and windows to false
			ac.MDM.EnabledAndConfigured = true
			ac.MDM.WindowsEnabledAndConfigured = false
			require.NoError(t, s.ds.SaveAppConfig(ctx, ac))
			acResp = checkAppConfig(t, true, false)                              // mac enabled, windows disabled
			compareMacOSSetupValues(t, acResp.MDM.MacOSSetup, ac.MDM.MacOSSetup) // directly applied

			// changing unrelated config doesn't cause validation error
			ac.OrgInfo.OrgName = "f1338"
			s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusOK, &acResp)
			acResp = checkAppConfig(t, true, false)                              // mac enabled, windows disabled
			compareMacOSSetupValues(t, acResp.MDM.MacOSSetup, ac.MDM.MacOSSetup) // no change
			require.Equal(t, "f1338", acResp.AppConfig.OrgInfo.OrgName)

			// disabling doesn't cause validation error
			ac.MDM.MacOSSetup = fleet.MacOSSetup{
				BootstrapPackage:            optjson.SetString(""),
				EnableEndUserAuthentication: false,
				MacOSSetupAssistant:         optjson.SetString(""),
			}
			s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusOK, &acResp)
			acResp = checkAppConfig(t, true, false)                              // only windows mdm enabled
			compareMacOSSetupValues(t, acResp.MDM.MacOSSetup, ac.MDM.MacOSSetup) // applied

			// bootstrap package and setup assistant were removed so reinsert records for next test
			insertBootstrapPackageAndSetupAssistant(t, nil)

			// enable MacOSSetup options succeeds because only Windows is disabled
			ac.MDM.MacOSSetup = fleet.MacOSSetup{
				BootstrapPackage:            optjson.SetString("foo"),
				EnableEndUserAuthentication: true,
				MacOSSetupAssistant:         optjson.SetString("bar"),
			}
			s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusOK, &acResp)
			acResp = checkAppConfig(t, true, false)                              // only windows enabled
			compareMacOSSetupValues(t, acResp.MDM.MacOSSetup, ac.MDM.MacOSSetup) // applied

			// directly set MDM.EnabledAndConfigured to false
			ac.MDM.EnabledAndConfigured = false
			require.NoError(t, s.ds.SaveAppConfig(ctx, ac))
			acResp = checkAppConfig(t, false, false)                             // both mac and windows mdm disabled
			compareMacOSSetupValues(t, acResp.MDM.MacOSSetup, ac.MDM.MacOSSetup) // still applied

			// changing unrelated config doesn't cause validation error
			ac.OrgInfo.OrgName = "f1339"
			s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusOK, &acResp)
			acResp = checkAppConfig(t, false, false)                             // both disabled
			compareMacOSSetupValues(t, acResp.MDM.MacOSSetup, ac.MDM.MacOSSetup) // no change
			require.Equal(t, "f1339", acResp.AppConfig.OrgInfo.OrgName)

			// setting macos setup empty values doesn't cause validation error when mdm is disabled
			ac.MDM.MacOSSetup = fleet.MacOSSetup{
				BootstrapPackage:            optjson.SetString(""),
				EnableEndUserAuthentication: false,
				MacOSSetupAssistant:         optjson.SetString(""),
			}
			s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusOK, &acResp)
			acResp = checkAppConfig(t, false, false)                             // both disabled
			compareMacOSSetupValues(t, acResp.MDM.MacOSSetup, ac.MDM.MacOSSetup) // applied

			// setting macos setup to non-empty values fails because mdm disabled
			ac.MDM.MacOSSetup = fleet.MacOSSetup{
				BootstrapPackage:            optjson.SetString("foo"),
				EnableEndUserAuthentication: true,
				MacOSSetupAssistant:         optjson.SetString("bar"),
			}
			s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusUnprocessableEntity, &acResp)
			acResp = checkAppConfig(t, false, false) // both disabled
		})

		t.Run("custom settings", func(t *testing.T) {
			t.Cleanup(func() {
				require.NoError(t, s.ds.SaveAppConfig(ctx, appConfig))
			})

			// initialize our test app config
			ac := appConfig.Copy()
			ac.AgentOptions = nil
			ac.MDM.MacOSSettings.CustomSettings = []fleet.MDMProfileSpec{}
			ac.MDM.WindowsSettings.CustomSettings = optjson.SetSlice([]fleet.MDMProfileSpec{})
			require.NoError(t, s.ds.SaveAppConfig(ctx, ac))
			acResp := checkAppConfig(t, true, true)
			require.Empty(t, acResp.MDM.MacOSSettings.CustomSettings)
			require.Empty(t, acResp.MDM.WindowsSettings.CustomSettings.Value)

			// add custom settings
			ac.MDM.MacOSSettings.CustomSettings = []fleet.MDMProfileSpec{{Path: "foo"}, {Path: "bar"}}
			ac.MDM.WindowsSettings.CustomSettings = optjson.SetSlice([]fleet.MDMProfileSpec{{Path: "baz"}, {Path: "zab"}})
			s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusOK, &acResp)
			acResp = checkAppConfig(t, true, true)                                                                                 // both mac and windows mdm enabled
			require.ElementsMatch(t, acResp.MDM.MacOSSettings.CustomSettings, ac.MDM.MacOSSettings.CustomSettings)                 // applied
			require.ElementsMatch(t, acResp.MDM.WindowsSettings.CustomSettings.Value, ac.MDM.WindowsSettings.CustomSettings.Value) // applied

			// directly set MDM.EnabledAndConfigured to false
			ac.MDM.EnabledAndConfigured = false
			require.NoError(t, s.ds.SaveAppConfig(ctx, ac))
			acResp = checkAppConfig(t, false, true)                                                                                // only windows mdm enabled
			require.ElementsMatch(t, acResp.MDM.MacOSSettings.CustomSettings, ac.MDM.MacOSSettings.CustomSettings)                 // still applied
			require.ElementsMatch(t, acResp.MDM.WindowsSettings.CustomSettings.Value, ac.MDM.WindowsSettings.CustomSettings.Value) // still applied

			// making an unrelated change should not cause validation error
			ac.OrgInfo.OrgName = "f1337"
			s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusOK, &acResp)
			acResp = checkAppConfig(t, false, true)                                                                                // only windows mdm enabled
			require.ElementsMatch(t, acResp.MDM.MacOSSettings.CustomSettings, ac.MDM.MacOSSettings.CustomSettings)                 // still applied
			require.ElementsMatch(t, acResp.MDM.WindowsSettings.CustomSettings.Value, ac.MDM.WindowsSettings.CustomSettings.Value) // still applied
			require.Equal(t, "f1337", acResp.AppConfig.OrgInfo.OrgName)

			// remove custom settings
			ac.MDM.MacOSSettings.CustomSettings = []fleet.MDMProfileSpec{}
			ac.MDM.WindowsSettings.CustomSettings = optjson.SetSlice([]fleet.MDMProfileSpec{})
			s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusOK, &acResp)
			acResp = checkAppConfig(t, false, true) // only windows mdm enabled
			require.Empty(t, acResp.MDM.MacOSSettings.CustomSettings)
			require.Empty(t, acResp.MDM.WindowsSettings.CustomSettings.Value)

			// add custom macOS settings fails because only windows is enabled
			ac.MDM.MacOSSettings.CustomSettings = []fleet.MDMProfileSpec{{Path: "foo"}, {Path: "bar"}}
			s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusUnprocessableEntity, &acResp)
			acResp = checkAppConfig(t, false, true) // only windows enabled
			require.Empty(t, acResp.MDM.MacOSSettings.CustomSettings)
			require.Empty(t, acResp.MDM.WindowsSettings.CustomSettings.Value)

			// add custom Windows settings suceeds because only macOS is disabled
			ac.MDM.MacOSSettings.CustomSettings = []fleet.MDMProfileSpec{}
			ac.MDM.WindowsSettings.CustomSettings = optjson.SetSlice([]fleet.MDMProfileSpec{{Path: "baz"}, {Path: "zab"}})
			s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusOK, &acResp)
			acResp = checkAppConfig(t, false, true)                                                                                // only windows mdm enabled
			require.ElementsMatch(t, acResp.MDM.WindowsSettings.CustomSettings.Value, ac.MDM.WindowsSettings.CustomSettings.Value) // applied
			require.Empty(t, acResp.MDM.MacOSSettings.CustomSettings)                                                              // no change

			// cleanup Windows settings
			ac.MDM.MacOSSettings.CustomSettings = []fleet.MDMProfileSpec{}
			ac.MDM.WindowsSettings.CustomSettings = optjson.SetSlice([]fleet.MDMProfileSpec{})
			s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusOK, &acResp)
			acResp = checkAppConfig(t, false, true) // only windows mdm enabled
			require.Empty(t, acResp.MDM.MacOSSettings.CustomSettings)
			require.Empty(t, acResp.MDM.WindowsSettings.CustomSettings.Value)

			// directly set MDM.EnabledAndConfigured to true and windows to false
			ac.MDM.EnabledAndConfigured = true
			ac.MDM.WindowsEnabledAndConfigured = false
			require.NoError(t, s.ds.SaveAppConfig(ctx, ac))
			acResp = checkAppConfig(t, true, false)                                                                // mac enabled, windows disabled
			require.ElementsMatch(t, acResp.MDM.MacOSSettings.CustomSettings, ac.MDM.MacOSSettings.CustomSettings) // directly applied
			require.Empty(t, acResp.MDM.WindowsSettings.CustomSettings.Value)                                      // still empty

			// add custom windows settings fails because only mac is enabled
			ac.MDM.WindowsSettings.CustomSettings = optjson.SetSlice([]fleet.MDMProfileSpec{{Path: "baz"}, {Path: "zab"}})
			s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusUnprocessableEntity, &acResp)
			acResp = checkAppConfig(t, true, false) // only mac enabled
			require.Empty(t, acResp.MDM.MacOSSettings.CustomSettings)
			require.Empty(t, acResp.MDM.WindowsSettings.CustomSettings.Value)
			// set this value to empty again so we can test other assertions assuming we're not setting it
			ac.MDM.WindowsSettings.CustomSettings = optjson.SetSlice([]fleet.MDMProfileSpec{})

			// changing unrelated config doesn't cause validation error
			ac.OrgInfo.OrgName = "f1338"
			s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusOK, &acResp)
			acResp = checkAppConfig(t, true, false)                                                                // mac enabled, windows disabled
			require.ElementsMatch(t, acResp.MDM.MacOSSettings.CustomSettings, ac.MDM.MacOSSettings.CustomSettings) // no change
			require.Empty(t, acResp.MDM.WindowsSettings.CustomSettings.Value)                                      // no change
			require.Equal(t, "f1338", acResp.AppConfig.OrgInfo.OrgName)

			// remove custom settings doesn't cause validation error
			ac.MDM.MacOSSettings.CustomSettings = []fleet.MDMProfileSpec{}
			s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusOK, &acResp)
			acResp = checkAppConfig(t, true, false) // only mac enabled
			require.Empty(t, acResp.MDM.MacOSSettings.CustomSettings)

			// add custom macOS settings suceeds because only Windows is disabled
			ac.MDM.MacOSSettings.CustomSettings = []fleet.MDMProfileSpec{{Path: "foo"}, {Path: "bar"}}
			s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusOK, &acResp)
			acResp = checkAppConfig(t, true, false)                                                                // mac enabled, windows disabled
			require.ElementsMatch(t, acResp.MDM.MacOSSettings.CustomSettings, ac.MDM.MacOSSettings.CustomSettings) // applied
			require.Empty(t, acResp.MDM.WindowsSettings.CustomSettings.Value)                                      // no change

			// temporarily enable and add custom settings for both platforms
			ac.MDM.EnabledAndConfigured = true
			ac.MDM.WindowsEnabledAndConfigured = true
			require.NoError(t, s.ds.SaveAppConfig(ctx, ac))
			acResp = checkAppConfig(t, true, true) // both mac and windows mdm enabled
			ac.MDM.MacOSSettings.CustomSettings = []fleet.MDMProfileSpec{{Path: "foo"}, {Path: "bar"}}
			ac.MDM.WindowsSettings.CustomSettings = optjson.SetSlice([]fleet.MDMProfileSpec{{Path: "baz"}, {Path: "zab"}})
			s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusOK, &acResp)
			acResp = checkAppConfig(t, true, true)                                                                                 // both mac and windows mdm enabled
			require.ElementsMatch(t, acResp.MDM.MacOSSettings.CustomSettings, ac.MDM.MacOSSettings.CustomSettings)                 // applied
			require.ElementsMatch(t, acResp.MDM.WindowsSettings.CustomSettings.Value, ac.MDM.WindowsSettings.CustomSettings.Value) // applied

			// directly set both configs to false
			ac.MDM.EnabledAndConfigured = false
			ac.MDM.WindowsEnabledAndConfigured = false
			require.NoError(t, s.ds.SaveAppConfig(ctx, ac))
			acResp = checkAppConfig(t, false, false)                                                                               // both mac and windows mdm disabled
			require.ElementsMatch(t, acResp.MDM.MacOSSettings.CustomSettings, ac.MDM.MacOSSettings.CustomSettings)                 // no change
			require.ElementsMatch(t, acResp.MDM.WindowsSettings.CustomSettings.Value, ac.MDM.WindowsSettings.CustomSettings.Value) // no change

			// changing unrelated config doesn't cause validation error
			ac.OrgInfo.OrgName = "f1339"
			s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusOK, &acResp)
			acResp = checkAppConfig(t, false, false)                                                                               // both disabled
			require.ElementsMatch(t, acResp.MDM.MacOSSettings.CustomSettings, ac.MDM.MacOSSettings.CustomSettings)                 // no change
			require.ElementsMatch(t, acResp.MDM.WindowsSettings.CustomSettings.Value, ac.MDM.WindowsSettings.CustomSettings.Value) // no change
			require.Equal(t, "f1339", acResp.AppConfig.OrgInfo.OrgName)

			// setting the same values is ok even if mdm is disabled
			ac.MDM.MacOSSettings.CustomSettings = []fleet.MDMProfileSpec{{Path: "foo"}, {Path: "bar"}}
			ac.MDM.WindowsSettings.CustomSettings = optjson.SetSlice([]fleet.MDMProfileSpec{{Path: "baz"}, {Path: "zab"}})
			s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusOK, &acResp)
			acResp = checkAppConfig(t, false, false)                                                                               // both disabled
			require.ElementsMatch(t, acResp.MDM.MacOSSettings.CustomSettings, ac.MDM.MacOSSettings.CustomSettings)                 // no change
			require.ElementsMatch(t, acResp.MDM.WindowsSettings.CustomSettings.Value, ac.MDM.WindowsSettings.CustomSettings.Value) // no change

			// setting different values fail even if mdm is disabled, and only some of the profiles have changed
			ac.MDM.MacOSSettings.CustomSettings = []fleet.MDMProfileSpec{{Path: "oof"}, {Path: "bar"}}
			ac.MDM.WindowsSettings.CustomSettings = optjson.SetSlice([]fleet.MDMProfileSpec{{Path: "foo"}, {Path: "zab"}})
			s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusUnprocessableEntity, &acResp)
			acResp = checkAppConfig(t, false, false) // both disabled
			// set the values back so we can compare them
			ac.MDM.MacOSSettings.CustomSettings = []fleet.MDMProfileSpec{{Path: "foo"}, {Path: "bar"}}
			ac.MDM.WindowsSettings.CustomSettings = optjson.SetSlice([]fleet.MDMProfileSpec{{Path: "baz"}, {Path: "zab"}})
			require.ElementsMatch(t, acResp.MDM.MacOSSettings.CustomSettings, ac.MDM.MacOSSettings.CustomSettings)                 // no change
			require.ElementsMatch(t, acResp.MDM.WindowsSettings.CustomSettings.Value, ac.MDM.WindowsSettings.CustomSettings.Value) // no change

			// setting empty values doesn't cause validation error when mdm is disabled
			ac.MDM.MacOSSettings.CustomSettings = []fleet.MDMProfileSpec{}
			ac.MDM.WindowsSettings.CustomSettings = optjson.SetSlice([]fleet.MDMProfileSpec{})
			s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusOK, &acResp)
			acResp = checkAppConfig(t, false, false) // both disabled
			require.Empty(t, acResp.MDM.MacOSSettings.CustomSettings)
			require.Empty(t, acResp.MDM.WindowsSettings.CustomSettings.Value)

			// setting non-empty values fails because mdm disabled
			ac.MDM.MacOSSettings.CustomSettings = []fleet.MDMProfileSpec{{Path: "foo"}, {Path: "bar"}}
			ac.MDM.WindowsSettings.CustomSettings = optjson.SetSlice([]fleet.MDMProfileSpec{{Path: "baz"}, {Path: "zab"}})
			s.DoJSON("PATCH", "/api/latest/fleet/config", ac, http.StatusUnprocessableEntity, &acResp)
			acResp = checkAppConfig(t, false, false) // both disabled
			require.Empty(t, acResp.MDM.MacOSSettings.CustomSettings)
			require.Empty(t, acResp.MDM.WindowsSettings.CustomSettings.Value)
		})
	})

	// TODO: Improve validations and related test coverage of team MDM config.
	// Some settings don't have MDMEnabledAndConfigured or WindowsMDMEnabledAndConfigured
	// validations currently. Either add vailidations and test them or test abscence
	// of validation. Also, the tests below only cover a limited set of permutations
	// compared to the app config tests above and should be expanded accordingly.
	t.Run("modify team", func(t *testing.T) {
		t.Cleanup(func() {
			require.NoError(t, s.ds.SaveAppConfig(ctx, appConfig))
		})

		checkTeam := func(t *testing.T, team *fleet.Team, checkMDM *fleet.TeamPayloadMDM) teamResponse {
			var wantDiskEncryption bool
			var wantMacOSSetup fleet.MacOSSetup
			if checkMDM != nil {
				if checkMDM.MacOSSetup != nil {
					wantMacOSSetup = *checkMDM.MacOSSetup
					// bootstrap package always ignored by modify team endpoint so expect original value
					wantMacOSSetup.BootstrapPackage = team.Config.MDM.MacOSSetup.BootstrapPackage
					// setup assistant always ignored by modify team endpoint so expect original value
					wantMacOSSetup.MacOSSetupAssistant = team.Config.MDM.MacOSSetup.MacOSSetupAssistant
				}
				wantDiskEncryption = checkMDM.EnableDiskEncryption.Value
			}

			var resp teamResponse
			s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), nil, http.StatusOK, &resp)
			require.Equal(t, team.Name, resp.Team.Name)
			require.Equal(t, wantDiskEncryption, resp.Team.Config.MDM.EnableDiskEncryption)
			require.Equal(t, wantMacOSSetup.BootstrapPackage.Value, resp.Team.Config.MDM.MacOSSetup.BootstrapPackage.Value)
			require.Equal(t, wantMacOSSetup.MacOSSetupAssistant.Value, resp.Team.Config.MDM.MacOSSetup.MacOSSetupAssistant.Value)
			require.Equal(t, wantMacOSSetup.EnableEndUserAuthentication, resp.Team.Config.MDM.MacOSSetup.EnableEndUserAuthentication)

			return resp
		}

		// initialize our test app config
		ac := appConfig.Copy()
		ac.AgentOptions = nil
		ac.MDM.EnabledAndConfigured = false
		ac.MDM.WindowsEnabledAndConfigured = false
		require.NoError(t, s.ds.SaveAppConfig(ctx, ac))
		checkAppConfig(t, false, false) // both mac and windows mdm disabled

		var createTeamResp teamResponse
		s.DoJSON("POST", "/api/latest/fleet/teams", createTeamRequest{fleet.TeamPayload{
			Name: ptr.String("Ninjas"),
			MDM:  &fleet.TeamPayloadMDM{EnableDiskEncryption: optjson.SetBool(true)}, // mdm is ignored by the create team endpoint
		}}, http.StatusOK, &createTeamResp)
		team := createTeamResp.Team
		getTeamResp := checkTeam(t, team, nil) // newly created team has empty mdm config

		t.Cleanup(func() {
			require.NoError(t, s.ds.DeleteTeam(ctx, team.ID))
		})

		// TODO: Add cases for other team MDM config (e.g., macos settings, macos updates,
		// migration) and for other permutations of starting values (see app config tests above).
		cases := []struct {
			name           string
			mdm            *fleet.TeamPayloadMDM
			expectedStatus int
		}{
			{
				"mdm empty",
				&fleet.TeamPayloadMDM{},
				http.StatusOK,
			},
			{
				"mdm all zero values",
				&fleet.TeamPayloadMDM{
					EnableDiskEncryption: optjson.SetBool(false),
					MacOSSetup: &fleet.MacOSSetup{
						BootstrapPackage:            optjson.SetString(""),
						EnableEndUserAuthentication: false,
						MacOSSetupAssistant:         optjson.SetString(""),
					},
				},
				http.StatusOK,
			},
			{
				"bootstrap package",
				&fleet.TeamPayloadMDM{
					MacOSSetup: &fleet.MacOSSetup{
						BootstrapPackage: optjson.SetString("some-package"),
					},
				},
				// bootstrap package is always ignored by the modify team endpoint
				http.StatusOK,
			},
			{
				"setup assistant",
				&fleet.TeamPayloadMDM{
					MacOSSetup: &fleet.MacOSSetup{
						MacOSSetupAssistant: optjson.SetString("some-setup-assistant"),
					},
				},
				// setup assistant is always ignored by the modify team endpoint
				http.StatusOK,
			},
			{
				"enable disk encryption",
				&fleet.TeamPayloadMDM{
					EnableDiskEncryption: optjson.SetBool(true),
				},
				// disk encryption requires mdm enabled and configured
				http.StatusUnprocessableEntity,
			},
			{
				"enable end user auth",
				&fleet.TeamPayloadMDM{
					MacOSSetup: &fleet.MacOSSetup{
						EnableEndUserAuthentication: true,
					},
				},
				// disk encryption requires mdm enabled and configured
				http.StatusUnprocessableEntity,
			},
		}

		for _, c := range cases {
			// TODO: Add tests for other combinations of mac and windows mdm enabled/disabled
			t.Run(c.name, func(t *testing.T) {
				checkAppConfig(t, false, false) // both mac and windows mdm disabled

				s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), fleet.TeamPayload{
					Name:        &team.Name,
					Description: ptr.String(c.name),
					MDM:         c.mdm,
				}, c.expectedStatus, &getTeamResp)

				if c.expectedStatus == http.StatusOK {
					getTeamResp = checkTeam(t, team, c.mdm)
					require.Equal(t, c.name, getTeamResp.Team.Description)
				} else {
					checkTeam(t, team, nil)
				}
			})
		}
	})

	// TODO: Improve validations and related test coverage of team MDM config.
	// Some settings don't have MDMEnabledAndConfigured or WindowsMDMEnabledAndConfigured
	// validations currently. Either add vailidations and test them or test abscence
	// of validation. Also, the tests below only cover a limited set of permutations
	// compared to the app config tests above and should be expanded accordingly.
	t.Run("edit team spec", func(t *testing.T) {
		t.Cleanup(func() {
			require.NoError(t, s.ds.SaveAppConfig(ctx, appConfig))
		})

		checkTeam := func(t *testing.T, team *fleet.Team, checkMDM *fleet.TeamSpecMDM) teamResponse {
			// TODO - remove check of disk encryption from this function entirely?
			// var wantDiskEncryption bool
			var wantMacOSSetup fleet.MacOSSetup
			if checkMDM != nil {
				wantMacOSSetup = checkMDM.MacOSSetup
				// wantDiskEncryption = checkMDM.EnableDiskEncryption.Value
			}

			var resp teamResponse
			s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/teams/%d", team.ID), nil, http.StatusOK, &resp)
			require.Equal(t, team.Name, resp.Team.Name)
			// require.Equal(t, wantDiskEncryption, resp.Team.Config.MDM.EnableDiskEncryption)
			require.Equal(t, wantMacOSSetup.BootstrapPackage.Value, resp.Team.Config.MDM.MacOSSetup.BootstrapPackage.Value)
			require.Equal(t, wantMacOSSetup.MacOSSetupAssistant.Value, resp.Team.Config.MDM.MacOSSetup.MacOSSetupAssistant.Value)
			require.Equal(t, wantMacOSSetup.EnableEndUserAuthentication, resp.Team.Config.MDM.MacOSSetup.EnableEndUserAuthentication)

			return resp
		}

		// initialize our test app config
		ac := appConfig.Copy()
		ac.AgentOptions = nil
		ac.MDM.EnabledAndConfigured = false
		ac.MDM.WindowsEnabledAndConfigured = false
		require.NoError(t, s.ds.SaveAppConfig(ctx, ac))
		checkAppConfig(t, false, false) // both mac and windows mdm disabled

		// create a team from spec
		tmSpecReq := applyTeamSpecsRequest{Specs: []*fleet.TeamSpec{{Name: "Pirates"}}}
		var tmSpecResp applyTeamSpecsResponse
		s.DoJSON("POST", "/api/latest/fleet/spec/teams", tmSpecReq, http.StatusOK, &tmSpecResp)
		teamID, ok := tmSpecResp.TeamIDsByName["Pirates"]
		require.True(t, ok)
		team := fleet.Team{ID: teamID, Name: "Pirates"}
		checkTeam(t, &team, nil) // newly created team has empty mdm config

		t.Cleanup(func() {
			require.NoError(t, s.ds.DeleteTeam(ctx, team.ID))
		})

		// TODO: Add cases for other team MDM config (e.g., macos settings, macos updates,
		// migration) and for other permutations of starting values (see app config tests above).
		cases := []struct {
			name           string
			mdm            *fleet.TeamSpecMDM
			expectedStatus int
		}{
			{
				"mdm empty",
				&fleet.TeamSpecMDM{},
				http.StatusOK,
			},
			{
				"mdm all zero values",
				&fleet.TeamSpecMDM{
					EnableDiskEncryption: optjson.SetBool(false),
					MacOSSetup: fleet.MacOSSetup{
						BootstrapPackage:            optjson.SetString(""),
						EnableEndUserAuthentication: false,
						MacOSSetupAssistant:         optjson.SetString(""),
					},
				},
				http.StatusOK,
			},
			{
				"bootstrap package",
				&fleet.TeamSpecMDM{
					MacOSSetup: fleet.MacOSSetup{
						BootstrapPackage: optjson.SetString("some-package"),
					},
				},
				// bootstrap package requires mdm enabled and configured
				http.StatusUnprocessableEntity,
			},
			{
				"setup assistant",
				&fleet.TeamSpecMDM{
					MacOSSetup: fleet.MacOSSetup{
						MacOSSetupAssistant: optjson.SetString("some-setup-assistant"),
					},
				},
				// setup assistant requires mdm enabled and configured
				http.StatusUnprocessableEntity,
			},
			{
				"enable disk encryption",
				&fleet.TeamSpecMDM{
					EnableDiskEncryption: optjson.SetBool(true),
				},
				// disk encryption does not require mdm enabled and configured
				http.StatusOK,
			},
			// Ian - this test still passes, that is, returns 4xx – perhaps related to one of the endpoints we still need to update
			{
				"enable end user auth",
				&fleet.TeamSpecMDM{
					MacOSSetup: fleet.MacOSSetup{
						EnableEndUserAuthentication: true,
					},
				},
				// disk encryption requires mdm enabled and configured
				http.StatusUnprocessableEntity,
			},
		}

		for _, c := range cases {
			// TODO: Add tests for other combinations of mac and windows mdm enabled/disabled
			t.Run(c.name, func(t *testing.T) {
				checkAppConfig(t, false, false) // both mac and windows mdm disabled

				tmSpecReq = applyTeamSpecsRequest{Specs: []*fleet.TeamSpec{{
					Name: team.Name,
					MDM:  *c.mdm,
				}}}
				s.DoJSON("POST", "/api/latest/fleet/spec/teams", tmSpecReq, c.expectedStatus, &tmSpecResp)

				if c.expectedStatus == http.StatusOK {
					checkTeam(t, &team, c.mdm)
				} else {
					checkTeam(t, &team, nil)
				}
			})
		}
	})
}

// ///////////////////////////////////////////////////////////////////////////
// Common helpers

func (s *integrationMDMTestSuite) runWorker() {
	err := s.worker.ProcessJobs(context.Background())
	require.NoError(s.T(), err)
	pending, err := s.ds.GetQueuedJobs(context.Background(), 1, time.Time{})
	require.NoError(s.T(), err)
	require.Empty(s.T(), pending)
}

// runWorkerUntilDone runs queued jobs until there are no more pending jobs
// this method handles the case when jobs could be queued asynchronously, such as during MDM enrollment
func (s *integrationMDMTestSuite) runWorkerUntilDone() {
	err := s.worker.ProcessJobs(s.T().Context())
	require.NoError(s.T(), err)

	// Retry checking for pending jobs
	require.Eventually(s.T(), func() bool {
		pending, err := s.ds.GetQueuedJobs(s.T().Context(), 1, time.Time{})
		require.NoError(s.T(), err)
		if len(pending) == 0 {
			return true
		}
		// Process any newly queued jobs
		s.T().Logf("Pending job: %s", pending[0].Name)
		err = s.worker.ProcessJobs(s.T().Context())
		return err == nil && len(pending) == 0
	}, 5*time.Second, 100*time.Millisecond, "jobs still pending after processing")
}

func (s *integrationMDMTestSuite) runDEPSchedule() {
	ctx := context.Background()
	fleetSyncer := apple_mdm.NewDEPService(s.ds, s.depStorage, s.logger)
	err := fleetSyncer.RunAssigner(ctx)
	require.NoError(s.T(), err)
}

func (s *integrationMDMTestSuite) runIntegrationsSchedule() {
	// FIXME: This pattern (which is being used in testing other schedules as well) seems cause issues
	// where a subsequent call attempts to trigger when the schedule's trigger channel is full and
	// schedule ignored the subsquent call (which is the documented behavior of the trigger).
	// In testing, this can cause the test to hang until the next scheduled run. It isn't a very
	// noticeable issue here since the intervals for these schedules are short.
	ch := make(chan bool)
	var once sync.Once
	s.onIntegrationsScheduleDone = func() {
		once.Do(func() { close(ch) })
	}
	_, err := s.integrationsSchedule.Trigger()
	require.NoError(s.T(), err)
	<-ch
}

func (s *integrationMDMTestSuite) awaitRunCleanupSchedule() {
	var wg sync.WaitGroup
	wg.Add(1)
	s.onCleanupScheduleDone = wg.Done
	_, err := s.cleanupsSchedule.Trigger()
	require.NoError(s.T(), err)
	// Add timeout detection
	done := make(chan struct{})
	go func() {
		wg.Wait()
		close(done)
	}()

	select {
	case <-done:
		// Completed successfully
	case <-time.After(5 * time.Minute):
		s.T().Fatalf("Cleanup schedule jobs timed out after 5 minutes")
	}
}

func (s *integrationMDMTestSuite) getRawTokenValue(content string) string {
	// Create a regex object with the defined pattern
	pattern := `inputToken.value\s*=\s*'([^']*)'`
	regex := regexp.MustCompile(pattern)

	// Find the submatch using the regex pattern
	submatches := regex.FindStringSubmatch(content)

	if len(submatches) >= 2 {
		// Extract the content from the submatch
		encodedToken := submatches[1]

		return encodedToken
	}

	return ""
}

func (s *integrationMDMTestSuite) isXMLTagPresent(xmlTag string, payload string) bool {
	regex := fmt.Sprintf("<%s.*>", xmlTag)
	matched, err := regexp.MatchString(regex, payload)
	if err != nil {
		return false
	}

	return matched
}

func (s *integrationMDMTestSuite) isXMLTagContentPresent(xmlTag string, payload string) bool {
	regex := fmt.Sprintf("<%s.*>(.+)</%s.*>", xmlTag, xmlTag)
	matched, err := regexp.MatchString(regex, payload)
	if err != nil {
		return false
	}

	return matched
}

func (s *integrationMDMTestSuite) checkIfXMLTagContains(xmlTag string, xmlContent string, payload string) bool {
	regex := fmt.Sprintf("<%s.*>.*%s.*</%s.*>", xmlTag, xmlContent, xmlTag)

	matched, err := regexp.MatchString(regex, payload)
	if err != nil || !matched {
		return false
	}

	return true
}

func (s *integrationMDMTestSuite) newGetPoliciesMsg(deviceToken bool, encodedBinToken string) ([]byte, error) {
	if len(encodedBinToken) == 0 {
		return nil, errors.New("encodedBinToken is empty")
	}

	// JWT token by default
	tokType := syncml.BinarySecurityAzureEnroll
	if deviceToken {
		tokType = syncml.BinarySecurityDeviceEnroll
	}

	return []byte(`
			<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns:wst="http://docs.oasis-open.org/ws-sx/ws-trust/200512" xmlns:ac="http://schemas.xmlsoap.org/ws/2006/12/authorization">
			<s:Header>
				<a:Action s:mustUnderstand="1">http://schemas.microsoft.com/windows/pki/2009/01/enrollmentpolicy/IPolicy/GetPolicies</a:Action>
				<a:MessageID>urn:uuid:148132ec-a575-4322-b01b-6172a9cf8478</a:MessageID>
				<a:ReplyTo>
				<a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
				</a:ReplyTo>
				<a:To s:mustUnderstand="1">https://mdmwindows.com/EnrollmentServer/Policy.svc</a:To>
				<wsse:Security s:mustUnderstand="1">
				<wsse:BinarySecurityToken ValueType="` + tokType + `" EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd#base64binary">` + encodedBinToken + `</wsse:BinarySecurityToken>
				</wsse:Security>
			</s:Header>
			<s:Body xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
				<GetPolicies xmlns="http://schemas.microsoft.com/windows/pki/2009/01/enrollmentpolicy">
				<client>
					<lastUpdate xsi:nil="true"/>
					<preferredLanguage xsi:nil="true"/>
				</client>
				<requestFilter xsi:nil="true"/>
				</GetPolicies>
			</s:Body>
			</s:Envelope>`), nil
}

func (s *integrationMDMTestSuite) newSecurityTokenMsg(encodedBinToken string, deviceToken bool, missingContextItem bool) ([]byte, error) {
	if len(encodedBinToken) == 0 {
		return nil, errors.New("encodedBinToken is empty")
	}

	var reqSecTokenContextItemDeviceType []byte
	if !missingContextItem {
		reqSecTokenContextItemDeviceType = []byte(
			`<ac:ContextItem Name="DeviceType">
			 <ac:Value>CIMClient_Windows</ac:Value>
			 </ac:ContextItem>`)
	}

	// JWT token by default
	tokType := syncml.BinarySecurityAzureEnroll
	if deviceToken {
		tokType = syncml.BinarySecurityDeviceEnroll
	}

	// Preparing the RequestSecurityToken Request message
	requestBytes := []byte(
		`<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns:wst="http://docs.oasis-open.org/ws-sx/ws-trust/200512" xmlns:ac="http://schemas.xmlsoap.org/ws/2006/12/authorization">
			<s:Header>
				<a:Action s:mustUnderstand="1">http://schemas.microsoft.com/windows/pki/2009/01/enrollment/RST/wstep</a:Action>
				<a:MessageID>urn:uuid:0d5a1441-5891-453b-becf-a2e5f6ea3749</a:MessageID>
				<a:ReplyTo>
				<a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
				</a:ReplyTo>
				<a:To s:mustUnderstand="1">https://mdmwindows.com/EnrollmentServer/Enrollment.svc</a:To>
				<wsse:Security s:mustUnderstand="1">
				<wsse:BinarySecurityToken ValueType="` + tokType + `" EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd#base64binary">` + encodedBinToken + `</wsse:BinarySecurityToken>
				</wsse:Security>
			</s:Header>
			<s:Body>
				<wst:RequestSecurityToken>
				<wst:TokenType>http://schemas.microsoft.com/5.0.0.0/ConfigurationManager/Enrollment/DeviceEnrollmentToken</wst:TokenType>
				<wst:RequestType>http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue</wst:RequestType>
				<wsse:BinarySecurityToken ValueType="http://schemas.microsoft.com/windows/pki/2009/01/enrollment#PKCS10" EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd#base64binary">MIICzjCCAboCAQAwSzFJMEcGA1UEAxNAMkI5QjUyQUMtREYzOC00MTYxLTgxNDItRjRCMUUwIURCMjU3QzNBMDg3NzhGNEZCNjFFMjc0OTA2NkMxRjI3ADCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKogsEpbKL8fuXpTNAE5RTZim8JO5CCpxj3z+SuWabs/s9Zse6RziKr12R4BXPiYE1zb8god4kXxet8x3ilGqAOoXKkdFTdNkdVa23PEMrIZSX5MuQ7mwGtctayARxmDvsWRF/icxJbqSO+bYIKvuifesOCHW2cJ1K+JSKijTMik1N8NFbLi5fg1J+xImT9dW1z2fLhQ7SNEMLosUPHsbU9WKoDBfnPsLHzmhM2IMw+5dICZRoxHZalh70FefBk0XoT8b6w4TIvc8572TyPvvdwhc5o/dvyR3nAwTmJpjBs1YhJfSdP+EBN1IC2T/i/mLNUuzUSC2OwiHPbZ6MMr/hUCAwEAAaBCMEAGCSqGSIb3DQEJDjEzMDEwLwYKKwYBBAGCN0IBAAQhREIyNTdDM0EwODc3OEY0RkI2MUUyNzQ5MDY2QzFGMjcAMAkGBSsOAwIdBQADggEBACQtxyy74sCQjZglwdh/Ggs6ofMvnWLMq9A9rGZyxAni66XqDUoOg5PzRtSt+Gv5vdLQyjsBYVzo42W2HCXLD2sErXWwh/w0k4H7vcRKgEqv6VYzpZ/YRVaewLYPcqo4g9NoXnbW345OPLwT3wFvVR5v7HnD8LB2wHcnMu0fAQORgafCRWJL1lgw8VZRaGw9BwQXCF/OrBNJP1ivgqtRdbSoH9TD4zivlFFa+8VDz76y2mpfo0NbbD+P0mh4r0FOJan3X9bLswOLFD6oTiyXHgcVSzLN0bQ6aQo0qKp3yFZYc8W4SgGdEl07IqNquKqJ/1fvmWxnXEbl3jXwb1efhbM=</wsse:BinarySecurityToken>
				<ac:AdditionalContext xmlns="http://schemas.xmlsoap.org/ws/2006/12/authorization">
					<ac:ContextItem Name="UXInitiated">
					<ac:Value>false</ac:Value>
					</ac:ContextItem>
					<ac:ContextItem Name="HWDevID">
					<ac:Value>CF1D12AA5AE42E47D52465E9A71316CAF3AFCC1D3088F230F4D50B371FB2256F</ac:Value>
					</ac:ContextItem>
					<ac:ContextItem Name="Locale">
					<ac:Value>en-US</ac:Value>
					</ac:ContextItem>
					<ac:ContextItem Name="TargetedUserLoggedIn">
					<ac:Value>true</ac:Value>
					</ac:ContextItem>
					<ac:ContextItem Name="OSEdition">
					<ac:Value>48</ac:Value>
					</ac:ContextItem>
					<ac:ContextItem Name="DeviceName">
					<ac:Value>DESKTOP-0C89RC0</ac:Value>
					</ac:ContextItem>
					<ac:ContextItem Name="MAC">
					<ac:Value>01-1C-29-7B-3E-1C</ac:Value>
					</ac:ContextItem>
					<ac:ContextItem Name="MAC">
					<ac:Value>01-0C-21-7B-3E-52</ac:Value>
					</ac:ContextItem>
					<ac:ContextItem Name="DeviceID">
					<ac:Value>AB157C3A18778F4FB21E2739066C1F27</ac:Value>
					</ac:ContextItem>
					<ac:ContextItem Name="EnrollmentType">
					<ac:Value>Full</ac:Value>
					</ac:ContextItem>
					` + string(reqSecTokenContextItemDeviceType) + `
					<ac:ContextItem Name="OSVersion">
					<ac:Value>10.0.19045.2965</ac:Value>
					</ac:ContextItem>
					<ac:ContextItem Name="ApplicationVersion">
					<ac:Value>10.0.19045.1965</ac:Value>
					</ac:ContextItem>
					<ac:ContextItem Name="NotInOobe">
					<ac:Value>false</ac:Value>
					</ac:ContextItem>
					<ac:ContextItem Name="RequestVersion">
					<ac:Value>5.0</ac:Value>
					</ac:ContextItem>
				</ac:AdditionalContext>
				</wst:RequestSecurityToken>
			</s:Body>
			</s:Envelope>
		`)

	return requestBytes, nil
}

func (s *integrationMDMTestSuite) newSyncMLUnenrollMsg(deviceID string, managementUrl string) ([]byte, error) {
	if len(managementUrl) == 0 {
		return nil, errors.New("managementUrl is empty")
	}

	return []byte(`
			 <SyncML xmlns="SYNCML:SYNCML1.2">
			<SyncHdr>
				<VerDTD>1.2</VerDTD>
				<VerProto>DM/1.2</VerProto>
				<SessionID>2</SessionID>
				<MsgID>1</MsgID>
				<Target>
				<LocURI>` + managementUrl + `</LocURI>
				</Target>
				<Source>
				<LocURI>` + deviceID + `</LocURI>
				</Source>
			</SyncHdr>
			<SyncBody>
				<Alert>
				<CmdID>2</CmdID>
				<Data>1201</Data>
				</Alert>
				<Alert>
				<CmdID>3</CmdID>
				<Data>1224</Data>
				<Item>
					<Meta>
					<Type xmlns="syncml:metinf">com.microsoft/MDM/LoginStatus</Type>
					</Meta>
					<Data>user</Data>
				</Item>
				</Alert>
				<Alert>
				<CmdID>4</CmdID>
				<Data>1226</Data>
				<Item>
					<Meta>
					<Type xmlns="syncml:metinf">com.microsoft:mdm.unenrollment.userrequest</Type>
					<Format xmlns="syncml:metinf">int</Format>
					</Meta>
					<Data>1</Data>
				</Item>
				</Alert>
				<Final/>
			</SyncBody>
			</SyncML>`), nil
}

func (s *integrationMDMTestSuite) checkMDMProfilesSummaries(t *testing.T, teamID *uint, expectedSummary fleet.MDMProfilesSummary, expectedAppleSummary *fleet.MDMProfilesSummary) {
	var queryParams []string
	if teamID != nil {
		queryParams = append(queryParams, "team_id", fmt.Sprintf("%d", *teamID))
	}

	if expectedAppleSummary != nil {
		var apple getMDMAppleProfilesSummaryResponse
		s.DoJSON("GET", "/api/v1/fleet/mdm/apple/profiles/summary", getMDMAppleProfilesSummaryRequest{}, http.StatusOK, &apple, queryParams...)
		require.Equal(t, expectedSummary.Failed, apple.Failed, "failed summary count doesn't match")
		require.Equal(t, expectedSummary.Pending, apple.Pending, "pending summary count doesn't match")
		require.Equal(t, expectedSummary.Verifying, apple.Verifying, "verifying summary count doesn't match")
		require.Equal(t, expectedSummary.Verified, apple.Verified, "verified summary count doesn't match")
	}

	var combined getMDMProfilesSummaryResponse
	s.DoJSON("GET", "/api/v1/fleet/configuration_profiles/summary", getMDMProfilesSummaryRequest{}, http.StatusOK, &combined, queryParams...)
	require.Equal(t, expectedSummary.Failed, combined.Failed, "failed summary count doesn't match")
	require.Equal(t, expectedSummary.Pending, combined.Pending, "pending summary count doesn't match")
	require.Equal(t, expectedSummary.Verifying, combined.Verifying, "verifying summary count doesn't match")
	require.Equal(t, expectedSummary.Verified, combined.Verified, "verified summary count doesn't match")
}

func (s *integrationMDMTestSuite) checkMDMDiskEncryptionSummaries(t *testing.T, teamID *uint, expectedSummary fleet.MDMDiskEncryptionSummary, checkFileVaultSummary bool) {
	var queryParams []string
	if teamID != nil {
		queryParams = append(queryParams, "team_id", fmt.Sprintf("%d", *teamID))
	}

	if checkFileVaultSummary {
		var fileVault getMDMAppleFileVaultSummaryResponse
		s.DoJSON("GET", "/api/v1/fleet/mdm/apple/filevault/summary", getMDMProfilesSummaryRequest{}, http.StatusOK, &fileVault, queryParams...)
		require.Equal(t, expectedSummary.Failed.MacOS, fileVault.Failed)
		require.Equal(t, expectedSummary.Enforcing.MacOS, fileVault.Enforcing)
		require.Equal(t, expectedSummary.ActionRequired.MacOS, fileVault.ActionRequired)
		require.Equal(t, expectedSummary.Verifying.MacOS, fileVault.Verifying)
		require.Equal(t, expectedSummary.Verified.MacOS, fileVault.Verified)
		require.Equal(t, expectedSummary.RemovingEnforcement.MacOS, fileVault.RemovingEnforcement)
	}

	var combined getMDMDiskEncryptionSummaryResponse
	s.DoJSON("GET", "/api/v1/fleet/disk_encryption", getMDMProfilesSummaryRequest{}, http.StatusOK, &combined, queryParams...)
	require.Equal(t, expectedSummary.Failed, combined.Failed)
	require.Equal(t, expectedSummary.Enforcing, combined.Enforcing)
	require.Equal(t, expectedSummary.ActionRequired, combined.ActionRequired)
	require.Equal(t, expectedSummary.Verifying, combined.Verifying)
	require.Equal(t, expectedSummary.Verified, combined.Verified)
	require.Equal(t, expectedSummary.RemovingEnforcement, combined.RemovingEnforcement)
}

func (s *integrationMDMTestSuite) TestWindowsFreshEnrollEmptyQuery() {
	t := s.T()
	host, _ := createWindowsHostThenEnrollMDM(s.ds, s.server.URL, t)

	// make sure we don't have any profiles
	s.Do(
		"POST",
		"/api/v1/fleet/mdm/profiles/batch",
		batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{}},
		http.StatusNoContent,
	)

	// Ensure we can read distributed queries for the host.
	err := s.ds.UpdateHostRefetchRequested(context.Background(), host.ID, true)
	require.NoError(t, err)

	s.lq.On("QueriesForHost", host.ID).Return(map[string]string{fmt.Sprintf("%d", host.ID): "SELECT 1 FROM osquery;"}, nil)

	req := getDistributedQueriesRequest{NodeKey: *host.NodeKey}
	var dqResp getDistributedQueriesResponse
	s.DoJSON("POST", "/api/osquery/distributed/read", req, http.StatusOK, &dqResp)
	require.NotContains(t, dqResp.Queries, "fleet_detail_query_mdm_config_profiles_windows")

	// add two profiles
	s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
		{Name: "N1", Contents: mobileconfigForTest("N1", "I1")},
		{Name: "N2", Contents: syncMLForTest("./Foo/Bar")},
	}}, http.StatusNoContent)

	req = getDistributedQueriesRequest{NodeKey: *host.NodeKey}
	dqResp = getDistributedQueriesResponse{}
	s.DoJSON("POST", "/api/osquery/distributed/read", req, http.StatusOK, &dqResp)
	require.Contains(t, dqResp.Queries, "fleet_detail_query_mdm_config_profiles_windows")
	require.NotEmpty(t, dqResp.Queries, "fleet_detail_query_mdm_config_profiles_windows")
}

func (s *integrationMDMTestSuite) TestManualEnrollmentCommands() {
	t := s.T()

	// create a device that's not enrolled into Fleet, it should get a command to
	// install fleetd
	mdmDevice := mdmtest.NewTestMDMClientAppleDirect(mdmtest.AppleEnrollInfo{
		SCEPChallenge: s.scepChallenge,
		SCEPURL:       s.server.URL + apple_mdm.SCEPPath,
		MDMURL:        s.server.URL + apple_mdm.MDMPath,
	}, "MacBookPro16,1")
	err := mdmDevice.Enroll()
	require.NoError(t, err)
	s.runWorker()
	checkInstallFleetdCommandSent(t, mdmDevice, true)

	// create a device that's enrolled into Fleet before turning on MDM features,
	// it should still get the command to install fleetd if turns on MDM.
	desktopToken := uuid.New().String()
	host := createOrbitEnrolledHost(t, "darwin", "h1", s.ds)
	err = s.ds.SetOrUpdateDeviceAuthToken(context.Background(), host.ID, desktopToken)
	require.NoError(t, err)
	mdmDevice = mdmtest.NewTestMDMClientAppleDesktopManual(s.server.URL, desktopToken)
	mdmDevice.UUID = host.UUID
	err = mdmDevice.Enroll()
	require.NoError(t, err)
	s.runWorker()
	checkInstallFleetdCommandSent(t, mdmDevice, true)
}

func (s *integrationMDMTestSuite) TestCustomConfigurationWebURL() {
	t := s.T()

	acResp := appConfigResponse{}
	s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp)

	s.enableABM(t.Name())
	var lastSubmittedProfile *godep.Profile
	configurationWebURLShouldBeEmpty := true
	s.mockDEPResponse(t.Name(), http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
		encoder := json.NewEncoder(w)

		switch r.URL.Path {
		case "/server/devices", "/devices/sync":
			encoder := json.NewEncoder(w)
			err := encoder.Encode(godep.DeviceResponse{
				Devices: []godep.Device{
					{
						SerialNumber: "FAKE-1",
						Model:        "Mac Mini",
						OS:           "osx",
						OpType:       "added",
					},
					{
						SerialNumber: "FAKE-2",
						Model:        "Mac Mini",
						OS:           "osx",
						OpType:       "added",
					},
				},
			})
			require.NoError(t, err)
		case "/profile":
			lastSubmittedProfile = &godep.Profile{}
			rawProfile, err := io.ReadAll(r.Body)
			require.NoError(t, err)
			err = json.Unmarshal(rawProfile, lastSubmittedProfile)
			require.NoError(t, err)

			// check that the urls are not empty and equal
			require.NotEmpty(t, lastSubmittedProfile.URL)
			if configurationWebURLShouldBeEmpty {
				assert.Empty(t, lastSubmittedProfile.ConfigurationWebURL)
			} else {
				require.NotEmpty(t, lastSubmittedProfile.ConfigurationWebURL)
				assert.Equal(t, lastSubmittedProfile.URL, lastSubmittedProfile.ConfigurationWebURL)
			}
			err = encoder.Encode(godep.ProfileResponse{ProfileUUID: uuid.New().String()})
			require.NoError(t, err)
		case "/profile/devices":
			b, err := io.ReadAll(r.Body)
			require.NoError(t, err)
			var prof profileAssignmentReq
			require.NoError(t, json.Unmarshal(b, &prof))
			var resp godep.ProfileResponse
			resp.ProfileUUID = prof.ProfileUUID
			resp.Devices = map[string]string{
				prof.Devices[0]: string(fleet.DEPAssignProfileResponseSuccess),
			}
			encoder := json.NewEncoder(w)
			err = encoder.Encode(resp)
			require.NoError(t, err)
		default:
			_, _ = w.Write([]byte(`{"auth_session_token": "xyz"}`))
		}
	}))

	// run once to ingest the devices
	s.runDEPSchedule()

	// disable first to make sure we start in the desired state
	acResp = appConfigResponse{}
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
		"mdm": {
			"macos_setup": {
				"enable_end_user_authentication": false
			}
		}
	}`), http.StatusOK, &acResp)

	// configure end-user authentication globally
	acResp = appConfigResponse{}
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
		"mdm": {
			"end_user_authentication": {
				"entity_id": "https://localhost:8080",
				"idp_name": "SimpleSAML",
				"metadata_url": "http://localhost:9080/simplesaml/saml2/idp/metadata.php"
			},
			"macos_setup": {
				"enable_end_user_authentication": true
			}
		}
	}`), http.StatusOK, &acResp)

	// assign the DEP profile and assert that contains the right values for the URL
	configurationWebURLShouldBeEmpty = false
	s.runWorker()
	require.NotNil(t, lastSubmittedProfile)
	require.Contains(t, lastSubmittedProfile.ConfigurationWebURL, acResp.ServerSettings.ServerURL+"/mdm/sso")

	// trying to set a custom configuration_web_url fails because end user authentication is enabled
	customSetupAsst := `{"configuration_web_url": "https://foo.example.com"}`
	var globalAsstResp createMDMAppleSetupAssistantResponse
	s.DoJSON("POST", "/api/latest/fleet/enrollment_profiles/automatic", createMDMAppleSetupAssistantRequest{
		TeamID:            nil,
		Name:              "no-team",
		EnrollmentProfile: json.RawMessage(customSetupAsst),
	}, http.StatusUnprocessableEntity, &globalAsstResp)

	// disable end user authentication
	acResp = appConfigResponse{}
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
		"mdm": {
			"end_user_authentication": {
				"entity_id": "",
				"idp_name": "",
				"metadata_url": ""
			},
			"macos_setup": {
				"enable_end_user_authentication": false
			}
		}
	}`), http.StatusOK, &acResp)

	// assign the DEP profile and assert that contains the right values for the URL
	configurationWebURLShouldBeEmpty = true
	s.runWorker()
	require.NotNil(t, lastSubmittedProfile)
	require.Contains(t, lastSubmittedProfile.URL, acResp.ServerSettings.ServerURL+"/api/mdm/apple/enroll?token=")

	// setting a custom configuration_web_url succeeds because user authentication is disabled
	globalAsstResp = createMDMAppleSetupAssistantResponse{}
	configurationWebURLShouldBeEmpty = false
	s.DoJSON("POST", "/api/latest/fleet/enrollment_profiles/automatic", createMDMAppleSetupAssistantRequest{
		TeamID:            nil,
		Name:              "no-team",
		EnrollmentProfile: json.RawMessage(customSetupAsst),
	}, http.StatusOK, &globalAsstResp)

	// assign the DEP profile and assert that contains the right values for the URL
	s.runWorker()
	require.NotNil(t, lastSubmittedProfile)
	require.Contains(t, lastSubmittedProfile.ConfigurationWebURL, "https://foo.example.com")

	// try to enable end user auth again, it fails because configuration_web_url is set
	acResp = appConfigResponse{}
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
		"mdm": {
			"end_user_authentication": {
				"entity_id": "https://localhost:8080",
				"idp_name": "SimpleSAML",
				"metadata_url": "http://localhost:9080/simplesaml/saml2/idp/metadata.php"
			},
			"macos_setup": {
				"enable_end_user_authentication": true
			}
		}
	}`), http.StatusUnprocessableEntity, &acResp)

	// create a team via spec
	teamSpecs := map[string]any{
		"specs": []any{
			map[string]any{
				"name": t.Name(),
				"mdm": map[string]any{
					"macos_setup": map[string]any{
						"enable_end_user_authentication": false,
					},
				},
			},
		},
	}
	var applyResp applyTeamSpecsResponse
	s.DoJSON("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK, &applyResp)
	require.Len(t, applyResp.TeamIDsByName, 1)
	teamID := applyResp.TeamIDsByName[t.Name()]

	// transfer a host to the team to ensure all ABM calls are made
	h, err := s.ds.HostByIdentifier(context.Background(), "FAKE-1")
	require.NoError(t, err)
	s.DoJSON("POST", "/api/latest/fleet/hosts/transfer", addHostsToTeamRequest{TeamID: &teamID, HostIDs: []uint{h.ID}}, http.StatusOK, &addHostsToTeamResponse{})

	// re-set the global state to configure MDM SSO
	err = s.ds.DeleteMDMAppleSetupAssistant(context.Background(), nil)
	require.NoError(t, err)
	acResp = appConfigResponse{}
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
		"mdm": {
			"end_user_authentication": {
				"entity_id": "https://localhost:8080",
				"idp_name": "SimpleSAML",
				"metadata_url": "http://localhost:9080/simplesaml/saml2/idp/metadata.php"
			},
			"macos_setup": {
				"enable_end_user_authentication": true
			}
		}
	}`), http.StatusOK, &acResp)

	// enable end user auth
	teamSpecs = map[string]any{
		"specs": []any{
			map[string]any{
				"name": t.Name(),
				"mdm": map[string]any{
					"macos_setup": map[string]any{
						"enable_end_user_authentication": true,
					},
				},
			},
		},
	}
	applyResp = applyTeamSpecsResponse{}
	s.DoJSON("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK, &applyResp)
	require.Len(t, applyResp.TeamIDsByName, 1)
	// assign the DEP profile and assert that contains the right values for the URL
	s.runWorker()
	require.Contains(t, lastSubmittedProfile.ConfigurationWebURL, acResp.ServerSettings.ServerURL+"/mdm/sso")

	// trying to set a custom configuration_web_url fails because end user authentication is enabled
	var tmAsstResp createMDMAppleSetupAssistantResponse
	s.DoJSON("POST", "/api/latest/fleet/enrollment_profiles/automatic", createMDMAppleSetupAssistantRequest{
		TeamID:            &teamID,
		Name:              t.Name(),
		EnrollmentProfile: json.RawMessage(customSetupAsst),
	}, http.StatusUnprocessableEntity, &tmAsstResp)

	// disable end user auth
	teamSpecs = map[string]any{
		"specs": []any{
			map[string]any{
				"name": t.Name(),
				"mdm": map[string]any{
					"macos_setup": map[string]any{
						"enable_end_user_authentication": false,
					},
				},
			},
		},
	}
	applyResp = applyTeamSpecsResponse{}
	s.DoJSON("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK, &applyResp)
	require.Len(t, applyResp.TeamIDsByName, 1)

	// assign the DEP profile and assert that contains the right values for the URL
	configurationWebURLShouldBeEmpty = true
	s.runWorker()
	require.Contains(t, lastSubmittedProfile.URL, acResp.ServerSettings.ServerURL+"/api/mdm/apple/enroll?token=")

	// setting configuration_web_url succeeds because end user authentication is disabled
	tmAsstResp = createMDMAppleSetupAssistantResponse{}
	configurationWebURLShouldBeEmpty = false
	s.DoJSON("POST", "/api/latest/fleet/enrollment_profiles/automatic", createMDMAppleSetupAssistantRequest{
		TeamID:            &teamID,
		Name:              t.Name(),
		EnrollmentProfile: json.RawMessage(customSetupAsst),
	}, http.StatusOK, &tmAsstResp)

	// assign the DEP profile and assert that contains the right values for the URL
	s.runWorker()
	require.Contains(t, lastSubmittedProfile.ConfigurationWebURL, "https://foo.example.com")

	// try to enable end user auth again, it fails because configuration_web_url is set
	teamSpecs = map[string]any{
		"specs": []any{
			map[string]any{
				"name": t.Name(),
				"mdm": map[string]any{
					"macos_setup": map[string]any{
						"enable_end_user_authentication": true,
					},
				},
			},
		},
	}
	applyResp = applyTeamSpecsResponse{}
	s.DoJSON("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusUnprocessableEntity, &applyResp)

	t.Cleanup(func() {
		acResp.MDM.MacOSSetup.EnableEndUserAuthentication = false
		err := s.ds.SaveAppConfig(context.Background(), &acResp.AppConfig)
		require.NoError(t, err)
	})
}

func (s *integrationMDMTestSuite) TestDontIgnoreAnyProfileErrors() {
	t := s.T()
	ctx := context.Background()

	appConfig, err := s.ds.AppConfig(ctx)
	require.NoError(t, err)
	appConfig.MDM.MacOSSetup = fleet.MacOSSetup{
		EnableEndUserAuthentication: false,
	}
	err = s.ds.SaveAppConfig(ctx, appConfig)
	require.NoError(t, err)

	// Create a host and a couple of profiles
	host, mdmDevice := createHostThenEnrollMDM(s.ds, s.server.URL, t)

	globalProfiles := [][]byte{
		mobileconfigForTest("N1", "I1"),
		mobileconfigForTest("N2", "I2"),
	}

	s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: globalProfiles}, http.StatusNoContent)
	s.awaitTriggerProfileSchedule(t)

	// The profiles should be associated with the host we made + the standard fleet configs
	profs, err := s.ds.GetHostMDMAppleProfiles(ctx, host.UUID)
	require.NoError(t, err)
	require.Len(t, profs, 4)

	// Acknowledge the profiles so we can mark them as verified
	cmd, err := mdmDevice.Idle()
	require.NoError(t, err)
	for cmd != nil {
		cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID)
		require.NoError(t, err)
	}

	require.NoError(t, apple_mdm.VerifyHostMDMProfiles(context.Background(), s.ds, host, map[string]*fleet.HostMacOSProfile{
		"I1": {Identifier: "I1", DisplayName: "I1", InstallDate: time.Now()},
		"I2": {Identifier: "I2", DisplayName: "I2", InstallDate: time.Now()},
		mobileconfig.FleetdConfigPayloadIdentifier:      {Identifier: mobileconfig.FleetdConfigPayloadIdentifier, DisplayName: "I2", InstallDate: time.Now()},
		mobileconfig.FleetCARootConfigPayloadIdentifier: {Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, DisplayName: "I2", InstallDate: time.Now()},
	}))

	// Check that the profile is marked as verified when fetching the host
	getHostResp := getHostResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &getHostResp)
	require.NotNil(t, getHostResp.Host.MDM.Profiles)
	for _, hm := range *getHostResp.Host.MDM.Profiles {
		require.Equal(t, fleet.MDMDeliveryVerified, *hm.Status)
	}

	// remove the profiles
	s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{}, http.StatusNoContent)
	s.awaitTriggerProfileSchedule(t)

	// On the host side, return errors for the two profile removal actions
	cmd, err = mdmDevice.Idle()
	require.NoError(t, err)
	for cmd != nil {
		if cmd.Command.RequestType == "RemoveProfile" {
			var errChain []mdm.ErrorChain
			var fullCmd micromdm.CommandPayload
			require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd))

			if fullCmd.Command.RemoveProfile.Identifier == "I1" {
				errChain = append(errChain, mdm.ErrorChain{ErrorCode: 89, ErrorDomain: "MDMClientError", USEnglishDescription: "Profile with identifier 'I1' not found."})
			} else {
				errChain = append(errChain, mdm.ErrorChain{ErrorCode: 96, ErrorDomain: "MDMClientError", USEnglishDescription: "Cannot replace profile 'I2' because it was not installed by the MDM server."})
			}
			cmd, err = mdmDevice.Err(cmd.CommandUUID, errChain)
			require.NoError(t, err)
			continue
		}
		cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID)
		require.NoError(t, err)
	}

	// get that host - it should report "failed" for the profiles and include the error message detail.
	expectedErrs := map[string]string{
		"N1": "Failed to remove: MDMClientError (89): Profile with identifier 'I1' not found.\n",
		"N2": "Failed to remove: MDMClientError (96): Cannot replace profile 'I2' because it was not installed by the MDM server.\n",
	}
	getHostResp = getHostResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &getHostResp)
	for _, hm := range *getHostResp.Host.MDM.Profiles {
		if wantErr, ok := expectedErrs[hm.Name]; ok {
			require.Equal(t, fleet.MDMDeliveryFailed, *hm.Status)
			require.Equal(t, wantErr, hm.Detail)
			continue
		}
		require.Equal(t, fleet.MDMDeliveryVerified, *hm.Status)
	}
}

func (s *integrationMDMTestSuite) TestMDMDiskEncryptionIssue16636() {
	// see https://github.com/fleetdm/fleet/issues/16636

	t := s.T()

	// send an empty patch object to ensure it's ok if not provided
	acResp := appConfigResponse{}
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{}`), http.StatusOK, &acResp)
	assert.False(t, acResp.MDM.EnableDiskEncryption.Value)
	s.assertConfigProfilesByIdentifier(nil, mobileconfig.FleetFileVaultPayloadIdentifier, false)

	// set it to true
	acResp = appConfigResponse{}
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
		"mdm": { "enable_disk_encryption": true }
  }`), http.StatusOK, &acResp)
	assert.True(t, acResp.MDM.EnableDiskEncryption.Value)
	s.assertConfigProfilesByIdentifier(nil, mobileconfig.FleetFileVaultPayloadIdentifier, true)

	// call PATCH with a null value, leaves it untouched
	acResp = appConfigResponse{}
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
		"mdm": { "enable_disk_encryption": null }
  }`), http.StatusOK, &acResp)
	assert.True(t, acResp.MDM.EnableDiskEncryption.Value)
	s.assertConfigProfilesByIdentifier(nil, mobileconfig.FleetFileVaultPayloadIdentifier, true)

	// send an empty patch object to ensure it doesn't alter the value
	acResp = appConfigResponse{}
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{}`), http.StatusOK, &acResp)
	assert.True(t, acResp.MDM.EnableDiskEncryption.Value)
	s.assertConfigProfilesByIdentifier(nil, mobileconfig.FleetFileVaultPayloadIdentifier, true)

	// setting it to false works
	acResp = appConfigResponse{}
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
		"mdm": { "enable_disk_encryption": false }
  }`), http.StatusOK, &acResp)
	assert.False(t, acResp.MDM.EnableDiskEncryption.Value)
	s.assertConfigProfilesByIdentifier(nil, mobileconfig.FleetFileVaultPayloadIdentifier, false)

	// send a null value again
	acResp = appConfigResponse{}
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
		"mdm": { "enable_disk_encryption": null }
  }`), http.StatusOK, &acResp)
	assert.False(t, acResp.MDM.EnableDiskEncryption.Value)
	s.assertConfigProfilesByIdentifier(nil, mobileconfig.FleetFileVaultPayloadIdentifier, false)

	// confirm via GET
	acResp = appConfigResponse{}
	s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp)
	assert.False(t, acResp.MDM.EnableDiskEncryption.Value)
	s.assertConfigProfilesByIdentifier(nil, mobileconfig.FleetFileVaultPayloadIdentifier, false)
}

func (s *integrationMDMTestSuite) TestIsServerBitlockerStatus() {
	t := s.T()
	ctx := context.Background()

	// create a server host that is not enrolled in MDM
	host := createOrbitEnrolledHost(t, "windows", "server-host", s.ds)
	require.NoError(t, s.ds.SetOrUpdateMDMData(ctx, host.ID, true, false, "", false, "", "", false))

	acResp := appConfigResponse{}
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
		"mdm": { "enable_disk_encryption": true }
  }`), http.StatusOK, &acResp)
	assert.True(t, acResp.MDM.EnableDiskEncryption.Value)

	var hr getHostResponse
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &hr)
	require.Nil(t, hr.Host.MDM.OSSettings.DiskEncryption.Status)

	// create a non-server host that is enrolled in MDM
	host2 := createOrbitEnrolledHost(t, "windows", "non-server-host", s.ds)
	require.NoError(t, s.ds.SetOrUpdateMDMData(ctx, host2.ID, false, true, "http://example.com", false, fleet.WellKnownMDMFleet, "", false))

	hr = getHostResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host2.ID), nil, http.StatusOK, &hr)
	require.NotNil(t, hr.Host.MDM.OSSettings.DiskEncryption.Status)
	require.Equal(t, fleet.DiskEncryptionEnforcing, *hr.Host.MDM.OSSettings.DiskEncryption.Status)
}

func (s *integrationMDMTestSuite) TestRemoveFailedProfiles() {
	t := s.T()

	teamName := t.Name()
	team := &fleet.Team{
		Name:        teamName,
		Description: "desc " + teamName,
	}

	var createTeamResp teamResponse
	s.DoJSON("POST", "/api/latest/fleet/teams", team, http.StatusOK, &createTeamResp)
	require.NotZero(t, createTeamResp.Team.ID)
	team.ID = createTeamResp.Team.ID

	host, mdmDevice := createHostThenEnrollMDM(s.ds, s.server.URL, t)

	ident := uuid.NewString()

	mdmDeviceRespond := func(device *mdmtest.TestAppleMDMClient) {
		cmd, err := device.Idle()
		require.NoError(t, err)
		for cmd != nil {
			if cmd.Command.RequestType == "InstallProfile" {
				var fullCmd micromdm.CommandPayload
				require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd))

				if strings.Contains(string(fullCmd.Command.InstallProfile.Payload), ident) {
					var errChain []mdm.ErrorChain
					errChain = append(errChain, mdm.ErrorChain{ErrorCode: -102, ErrorDomain: "CPProfile", USEnglishDescription: "The profile is either missing some required information, or contains information in an invalid format."})
					cmd, err = device.Err(cmd.CommandUUID, errChain)
					require.NoError(t, err)
					continue
				}
			}
			cmd, err = device.Acknowledge(cmd.CommandUUID)
			require.NoError(t, err)
		}
	}

	globalProfiles := [][]byte{
		mobileconfigForTest("N1", ident),
		mobileconfigForTest("N2", "I2"),
	}
	s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: globalProfiles}, http.StatusNoContent)
	s.awaitTriggerProfileSchedule(t)
	mdmDeviceRespond(mdmDevice)
	require.NoError(t, apple_mdm.VerifyHostMDMProfiles(context.Background(), s.ds, host, map[string]*fleet.HostMacOSProfile{
		"I2": {Identifier: "I2", DisplayName: "I2", InstallDate: time.Now()},
		"I1": {Identifier: "I1", DisplayName: "I1", InstallDate: time.Now()},
	}))

	// Do another trigger + command fetching cycle, since we retry when a profile fails on install.
	s.awaitTriggerProfileSchedule(t)
	mdmDeviceRespond(mdmDevice)

	require.NoError(t, apple_mdm.VerifyHostMDMProfiles(context.Background(), s.ds, host, map[string]*fleet.HostMacOSProfile{
		"I1": {Identifier: "I1", DisplayName: "I1", InstallDate: time.Now()},
		mobileconfig.FleetdConfigPayloadIdentifier:      {Identifier: mobileconfig.FleetdConfigPayloadIdentifier, DisplayName: "dn1", InstallDate: time.Now()},
		mobileconfig.FleetCARootConfigPayloadIdentifier: {Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, DisplayName: "dn2", InstallDate: time.Now()},
	}))

	// Check that the profile is marked as failed when fetching the host
	getHostResp := getHostResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &getHostResp)
	require.NotNil(t, getHostResp.Host.MDM.Profiles)
	require.Len(t, *getHostResp.Host.MDM.Profiles, 4)
	for _, hm := range *getHostResp.Host.MDM.Profiles {
		if hm.Name == "N1" {
			require.Equal(t, fleet.MDMDeliveryFailed, *hm.Status)
			continue
		}

		require.Equal(t, fleet.MDMDeliveryVerified, *hm.Status)
	}

	// transfer host to a team without the failed profile

	s.DoJSON("POST", "/api/latest/fleet/hosts/transfer", addHostsToTeamRequest{TeamID: &team.ID, HostIDs: []uint{host.ID}}, http.StatusOK, &addHostsToTeamResponse{})
	s.awaitTriggerProfileSchedule(t)

	// confirm that we remove the failed profile
	getHostResp = getHostResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &getHostResp)
	require.NotNil(t, getHostResp.Host.MDM.Profiles)
	require.Len(t, *getHostResp.Host.MDM.Profiles, 3) // This would be 4 if we hadn't deleted the profile that failed to install.
	for _, hm := range *getHostResp.Host.MDM.Profiles {
		require.NotEqual(t, "N1", hm.Name)
	}

	// Test case where the profile never makes it to the host at all
	host, _ = createHostThenEnrollMDM(s.ds, s.server.URL, t)
	ident = uuid.NewString()

	globalProfiles = [][]byte{
		mobileconfigForTest("N3", ident),
	}
	s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: globalProfiles}, http.StatusNoContent)
	s.awaitTriggerProfileSchedule(t)

	getHostResp = getHostResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &getHostResp)
	require.NotNil(t, getHostResp.Host.MDM.Profiles)
	require.Len(t, *getHostResp.Host.MDM.Profiles, 3)
	var profUUID string
	for _, hm := range *getHostResp.Host.MDM.Profiles {
		require.Equal(t, fleet.MDMDeliveryPending, *hm.Status)
		if hm.Name == "N3" {
			profUUID = hm.ProfileUUID
		}
	}

	// delete the custom profile
	s.Do("DELETE", fmt.Sprintf("/api/latest/fleet/mdm/profiles/%s", profUUID), &deleteMDMAppleConfigProfileRequest{}, http.StatusOK)
	s.awaitTriggerProfileSchedule(t)

	getHostResp = getHostResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &getHostResp)
	require.NotNil(t, getHostResp.Host.MDM.Profiles)
	// Since Fleet doesn't know for sure whether profile was installed or not, it sends a remove command just in case.
	require.Len(t, *getHostResp.Host.MDM.Profiles, 3)
	for _, hm := range *getHostResp.Host.MDM.Profiles {
		require.Equal(t, fleet.MDMDeliveryPending, *hm.Status)
		if hm.Name == "N3" {
			assert.Equal(t, fleet.MDMOperationTypeRemove, hm.OperationType)
		}
	}
}

func (s *integrationMDMTestSuite) TestABMAssetManagement() {
	t := s.T()
	ctx := context.Background()

	s.enableABM(t.Name())

	// Validate error when server private key not set
	testSetEmptyPrivateKey = true
	t.Cleanup(func() { testSetEmptyPrivateKey = false })

	r := s.Do("GET", "/api/latest/fleet/mdm/apple/abm_public_key", generateABMKeyPairResponse{}, http.StatusInternalServerError)
	require.Contains(t, extractServerErrorText(r.Body), "Couldn't download public key. Missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key")
	testSetEmptyPrivateKey = false

	// grab the current public key
	var abmResp generateABMKeyPairResponse
	s.DoJSON("GET", "/api/latest/fleet/mdm/apple/abm_public_key", nil, http.StatusOK, &abmResp)
	require.Nil(t, abmResp.Err)
	require.NotEmpty(t, abmResp.PublicKey)

	var tokensResp listABMTokensResponse
	s.DoJSON("GET", "/api/latest/fleet/abm_tokens", nil, http.StatusOK, &tokensResp)
	tok := s.getABMTokenByName(t.Name(), tokensResp.Tokens)

	// disable ABM
	s.Do("DELETE", fmt.Sprintf("/api/latest/fleet/abm_tokens/%d", tok.ID), nil, http.StatusNoContent)
	tok, err := s.ds.GetABMTokenByOrgName(ctx, t.Name())
	var nfe fleet.NotFoundError
	require.ErrorAs(t, err, &nfe)
	require.Nil(t, tok)

	// try to upload an invalid token
	s.uploadABMToken([]byte("foo"), http.StatusBadRequest, "Please provide a valid token from Apple Business Manager")

	// enable ABM again
	var newABMResp generateABMKeyPairResponse
	s.DoJSON("GET", "/api/latest/fleet/mdm/apple/abm_public_key", nil, http.StatusOK, &newABMResp)
	require.Nil(t, newABMResp.Err)
	require.NotEmpty(t, newABMResp.PublicKey)
	block, _ := pem.Decode(newABMResp.PublicKey)
	require.NotNil(t, block)
	require.Equal(t, "CERTIFICATE", block.Type)

	// we should always return the same values to support renewing the token
	var renewABMResp generateABMKeyPairResponse
	s.DoJSON("GET", "/api/latest/fleet/mdm/apple/abm_public_key", nil, http.StatusOK, &renewABMResp)
	require.Nil(t, renewABMResp.Err)
	require.NotEmpty(t, renewABMResp.PublicKey)
	require.Equal(t, renewABMResp.PublicKey, newABMResp.PublicKey)

	// simulate a renew flow
	s.enableABM(t.Name())
}

func (s *integrationMDMTestSuite) enableABM(orgName string) *fleet.ABMToken {
	t := s.T()
	var abmResp generateABMKeyPairResponse
	s.DoJSON("GET", "/api/latest/fleet/mdm/apple/abm_public_key", nil, http.StatusOK, &abmResp)
	require.Nil(t, abmResp.Err)
	require.NotEmpty(t, abmResp.PublicKey)
	block, _ := pem.Decode(abmResp.PublicKey)
	require.NotNil(t, block)
	require.Equal(t, "CERTIFICATE", block.Type)

	// try to upload an invalid token
	s.uploadABMToken([]byte("foo"), http.StatusBadRequest, "Invalid token. Please provide a valid token from Apple Business Manager.")

	// generate a mock token and encrypt it using the public key
	testBMToken := &nanodep_client.OAuth1Tokens{
		ConsumerKey:    "test_consumer",
		ConsumerSecret: "test_secret",
		AccessToken:    "test_access_token",
		AccessSecret:   "test_access_secret",
		// Use 2037 to avoid Y2K38 issues with 32-bit timestamps and potential MySQL date validation issues
		AccessTokenExpiry: time.Date(2037, 1, 1, 0, 0, 0, 0, time.UTC),
	}

	rawToken, err := json.Marshal(testBMToken)
	require.NoError(t, err)

	cert, err := x509.ParseCertificate(block.Bytes)
	require.NoError(t, err)

	smimeToken := fmt.Sprintf(
		"Content-Type: text/plain;charset=UTF-8\r\n"+
			"Content-Transfer-Encoding: 7bit\r\n"+
			"\r\n%s", rawToken,
	)

	encryptedToken, err := pkcs7.Encrypt([]byte(smimeToken), []*x509.Certificate{cert})
	require.NoError(t, err)

	s.mockDEPResponse(apple_mdm.UnsavedABMTokenOrgName, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
		switch r.URL.Path {
		case "/session":
			_, _ = w.Write([]byte(`{"auth_session_token": "xyz"}`))
		case "/account":
			_, _ = w.Write([]byte(fmt.Sprintf(`{"admin_id": "abc", "org_name": %q}`, orgName)))
		}
	}))

	// upload the encrypted token
	smimeMessage := fmt.Sprintf(
		"Content-Type: application/pkcs7-mime; name=\"smime.p7m\"; smime-type=enveloped-data\r\n"+
			"Content-Transfer-Encoding: base64\r\n"+
			"Content-Disposition: attachment; filename=\"smime.p7m\"\r\n"+
			"Content-Description: S/MIME Encrypted Message\r\n"+
			"\r\n%s", base64.StdEncoding.EncodeToString(encryptedToken))
	s.uploadABMToken([]byte(smimeMessage), http.StatusOK, "")

	// verify that all the secrets are in the db
	ctx := context.Background()
	assets, err := s.ds.GetAllMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{
		fleet.MDMAssetABMCert,
		fleet.MDMAssetABMKey,
	}, nil)
	require.NoError(t, err)
	require.Len(t, assets, 2)
	require.Equal(t, abmResp.PublicKey, assets[fleet.MDMAssetABMCert].Value)

	tok, err := s.ds.GetABMTokenByOrgName(ctx, orgName)
	require.NoError(t, err)
	require.Equal(t, orgName, tok.OrganizationName)

	// do a dummy call so the nanodep client updates the org name in
	// nano_dep_names, and leave the mock set with a dummy response
	s.mockDEPResponse(orgName, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
		switch r.URL.Path {
		case "/session":
			_, _ = w.Write([]byte(`{"auth_session_token": "xyz"}`))
		case "/account":
			_, _ = w.Write([]byte(fmt.Sprintf(`{"admin_id": "abc", "org_name": %q}`, orgName)))
		default:
			_, _ = w.Write([]byte(`{}`))
		}
	}))
	depClient := apple_mdm.NewDEPClient(s.depStorage, s.ds, s.logger)
	_, err = depClient.AccountDetail(ctx, orgName)
	require.NoError(t, err)
	return tok
}

func (s *integrationMDMTestSuite) appleCoreCertsSetup() {
	t := s.T()
	ctx := context.Background()

	// Successful request
	resp := getMDMAppleCSRResponse{}
	s.SucceedNextCSRRequest()
	s.DoJSON("GET", "/api/latest/fleet/mdm/apple/request_csr", getMDMAppleCSRRequest{}, http.StatusOK, &resp)
	require.NotNil(t, resp.CSR)
	block, _ := pem.Decode(resp.CSR)
	require.NotNil(t, block)
	require.Equal(t, "CERTIFICATE REQUEST", block.Type)

	// Check that we created the right assets
	originalAssets, err := s.ds.GetAllMDMConfigAssetsByName(ctx,
		[]fleet.MDMAssetName{fleet.MDMAssetCACert, fleet.MDMAssetCAKey, fleet.MDMAssetAPNSKey}, nil)
	require.NoError(t, err)
	require.Len(t, originalAssets, 3)

	resp = getMDMAppleCSRResponse{}
	s.SucceedNextCSRRequest()
	s.DoJSON("GET", "/api/latest/fleet/mdm/apple/request_csr", getMDMAppleCSRRequest{}, http.StatusOK, &resp)
	require.NotNil(t, resp.CSR)
	block, _ = pem.Decode(resp.CSR)
	require.NotNil(t, block)
	require.Equal(t, "CERTIFICATE REQUEST", block.Type)

	// Check that the assets stayed the same in the subsequent call
	assets, err := s.ds.GetAllMDMConfigAssetsByName(ctx,
		[]fleet.MDMAssetName{fleet.MDMAssetCACert, fleet.MDMAssetCAKey, fleet.MDMAssetAPNSKey}, nil)
	require.NoError(t, err)
	require.Equal(t, originalAssets, assets)

	// Successfully upload an APNS cert
	csr, err := x509.ParseCertificateRequest(block.Bytes)
	require.NoError(t, err)

	certTemplate := &x509.Certificate{
		SerialNumber:          big.NewInt(12345678),
		NotBefore:             time.Now(),
		NotAfter:              time.Now().Add(365 * 24 * time.Hour),
		KeyUsage:              x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
		BasicConstraintsValid: true,
		Subject: pkix.Name{
			CommonName: "Fleet",
			ExtraNames: []pkix.AttributeTypeAndValue{
				{
					Type:  asn1.ObjectIdentifier{0, 9, 2342, 19200300, 100, 1, 1},
					Value: "com.apple.mgmt.Example",
				},
			},
		},
	}

	testCert, testKey, err := apple_mdm.NewSCEPCACertKey()
	require.NoError(s.T(), err)
	certDER, err := x509.CreateCertificate(rand.Reader, certTemplate, testCert, csr.PublicKey, testKey)
	require.NoError(t, err)
	certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
	s.uploadDataViaForm("/api/latest/fleet/mdm/apple/apns_certificate", "certificate", "certificate.pem", certPEM, http.StatusAccepted, "", nil)

	assets, err = s.ds.GetAllMDMConfigAssetsByName(ctx,
		[]fleet.MDMAssetName{fleet.MDMAssetCACert, fleet.MDMAssetCAKey, fleet.MDMAssetAPNSKey, fleet.MDMAssetAPNSCert}, nil)
	require.NoError(t, err)
	require.Len(t, assets, 4)
}

func (s *integrationMDMTestSuite) uploadABMToken(encryptedToken []byte, expectedStatus int, wantErr string) {
	t := s.T()

	var b bytes.Buffer
	w := multipart.NewWriter(&b)

	// add the package field
	fw, err := w.CreateFormFile("token", "token.tok")
	require.NoError(t, err)
	_, err = io.Copy(fw, bytes.NewBuffer(encryptedToken))
	require.NoError(t, err)

	w.Close()

	headers := map[string]string{
		"Content-Type":  w.FormDataContentType(),
		"Accept":        "application/json",
		"Authorization": fmt.Sprintf("Bearer %s", s.token),
	}

	res := s.DoRawWithHeaders("POST", "/api/latest/fleet/abm_tokens", b.Bytes(), expectedStatus, headers)
	if wantErr != "" {
		errMsg := extractServerErrorText(res.Body)
		assert.Contains(t, errMsg, wantErr)
	}
}

func (s *integrationMDMTestSuite) TestSilentMigrationGotchas() {
	t := s.T()
	ctx := context.Background()

	host := createOrbitEnrolledHost(t, "darwin", t.Name(), s.ds)
	// set the host as enrolled in a third-party MDM
	err := s.ds.SetOrUpdateMDMData(ctx, host.ID, true, true, "https://foo.com", false, fleet.WellKnownMDMSimpleMDM, "", false)
	require.NoError(t, err)

	var hostResp getHostResponse
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &hostResp)
	require.NotNil(t, hostResp.Host)
	require.NotNil(t, hostResp.Host.MDM.ConnectedToFleet)
	require.False(t, *hostResp.Host.MDM.ConnectedToFleet)

	// simulate that the device is assigned to Fleet in ABM
	s.enableABM(t.Name())
	s.mockDEPResponse(t.Name(), http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
		switch r.URL.Path {
		case "/session":
			_, _ = w.Write([]byte(`{"auth_session_token": "xyz"}`))
		case "/profile":
			encoder := json.NewEncoder(w)
			err := encoder.Encode(godep.ProfileResponse{ProfileUUID: "abc"})
			require.NoError(t, err)
		case "/server/devices", "/devices/sync":
			encoder := json.NewEncoder(w)
			err := encoder.Encode(godep.DeviceResponse{
				Devices: []godep.Device{
					{
						SerialNumber: host.HardwareSerial,
						Model:        "Mac Mini",
						OS:           "osx",
						OpType:       "added",
					},
				},
			})
			require.NoError(t, err)

		case "/profile/devices":
			b, err := io.ReadAll(r.Body)
			require.NoError(t, err)
			var prof profileAssignmentReq
			require.NoError(t, json.Unmarshal(b, &prof))
			var resp godep.ProfileResponse
			resp.ProfileUUID = prof.ProfileUUID
			resp.Devices = map[string]string{
				prof.Devices[0]: string(fleet.DEPAssignProfileResponseSuccess),
			}
			encoder := json.NewEncoder(w)
			err = encoder.Encode(resp)
			require.NoError(t, err)
		}
	}))
	s.runDEPSchedule()

	// enable migrations
	acResp := appConfigResponse{}
	s.DoJSON("PATCH", "/api/v1/fleet/config", json.RawMessage(`{
		"mdm": { "macos_migration": { "enable": true, "mode": "voluntary", "webhook_url": "https://example.com" } }
	}`), http.StatusOK, &acResp)

	// orbit config asks for a migration but not to renew enrollment profile
	resp := orbitGetConfigResponse{}
	s.DoJSON("POST", "/api/fleet/orbit/config", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *host.OrbitNodeKey)), http.StatusOK, &resp)
	require.False(t, resp.Notifications.RenewEnrollmentProfile)
	require.True(t, resp.Notifications.NeedsMDMMigration)

	// simulate that's actually enrolled to Fleet under the hood
	mdmDevice := mdmtest.NewTestMDMClientAppleDirect(mdmtest.AppleEnrollInfo{
		SCEPChallenge: s.scepChallenge,
		SCEPURL:       s.server.URL + apple_mdm.SCEPPath,
		MDMURL:        s.server.URL + apple_mdm.MDMPath,
	}, "MacBookPro16,1")
	// by default the mdm test client will have a random uuid and serial, but we want
	// it to match with the previously created host
	mdmDevice.SerialNumber = host.HardwareSerial
	mdmDevice.UUID = host.UUID
	err = mdmDevice.Enroll()
	require.NoError(t, err)

	// host response says that it's connected to Fleet (this just checks that the
	// device exists and is enabled in nano_enrollments, which it is thanks to
	// the mdmDevice.Enroll call above - the row is created during TokenUpdate).
	hostResp = getHostResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &hostResp)
	require.NotNil(t, hostResp.Host)
	require.NotNil(t, hostResp.Host.MDM.ConnectedToFleet)
	require.True(t, *hostResp.Host.MDM.ConnectedToFleet)

	// orbit config asks for a migration because user migrations are enabled, but no ask to renew the enrollment profile.
	resp = orbitGetConfigResponse{}
	s.DoJSON("POST", "/api/fleet/orbit/config", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *host.OrbitNodeKey)), http.StatusOK, &resp)
	require.False(t, resp.Notifications.RenewEnrollmentProfile)
	require.True(t, resp.Notifications.NeedsMDMMigration)

	// set an enroll secret so the fleetd profile is delivered
	var applyResp applyEnrollSecretSpecResponse
	s.DoJSON("POST", "/api/latest/fleet/spec/enroll_secret", applyEnrollSecretSpecRequest{
		Spec: &fleet.EnrollSecretSpec{
			Secrets: []*fleet.EnrollSecret{{Secret: t.Name()}},
		},
	}, http.StatusOK, &applyResp)

	// trigger the profile cron
	s.awaitTriggerProfileSchedule(t)

	// explicitly run the worker, which will send the install fleetd command
	// because the device is ADE-enrolled (due to the simulation of it being
	// ingested from ABM)
	s.runWorker()

	var installEnterpriseCount int
	installs := [][]byte{}
	cmd, err := mdmDevice.Idle()
	require.NoError(t, err)

	for cmd != nil {
		if cmd.Command.RequestType == "InstallEnterpriseApplication" {
			installEnterpriseCount++
		} else {
			require.Equal(t, "InstallProfile", cmd.Command.RequestType)
			installs = append(installs, cmd.Raw)
		}
		cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID)
		require.NoError(t, err)
	}

	require.Equal(t, 1, installEnterpriseCount)
	require.Len(t, installs, 2) // fleetd profile and root cert profile

	// trigger the scep renewals cron
	cert, key, err := generateCertWithAPNsTopic()
	require.NoError(t, err)
	fleetCfg := config.TestConfig()
	config.SetTestMDMConfig(s.T(), &fleetCfg, cert, key, "")
	logger := kitlog.NewJSONLogger(os.Stdout)
	err = RenewSCEPCertificates(ctx, logger, s.ds, &fleetCfg, s.mdmCommander)
	require.NoError(t, err)

	// no new commands were enqueued
	cmd, err = mdmDevice.Idle()
	require.NoError(t, err)
	require.Nil(t, cmd)

	// set the host as completely unenrolled
	err = s.ds.SetOrUpdateMDMData(ctx, host.ID, false, false, "", false, "", "", false)
	require.NoError(t, err)
	// orbit config asks to renew the enrollment profile, migration is not needed anymore so it's false
	resp = orbitGetConfigResponse{}
	s.DoJSON("POST", "/api/fleet/orbit/config", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *host.OrbitNodeKey)), http.StatusOK, &resp)
	require.True(t, resp.Notifications.RenewEnrollmentProfile)
	require.False(t, resp.Notifications.NeedsMDMMigration)

	// with migrations disabled, it still asks to renew the enrollment profile
	acResp = appConfigResponse{}
	s.DoJSON("PATCH", "/api/v1/fleet/config", json.RawMessage(`{
		"mdm": { "macos_migration": { "enable": false } }
	}`), http.StatusOK, &acResp)
	resp = orbitGetConfigResponse{}
	s.DoJSON("POST", "/api/fleet/orbit/config", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *host.OrbitNodeKey)), http.StatusOK, &resp)
	require.True(t, resp.Notifications.RenewEnrollmentProfile)
	require.False(t, resp.Notifications.NeedsMDMMigration)
}

func (s *integrationMDMTestSuite) TestAPNsPushCron() {
	t := s.T()
	ctx := context.Background()

	s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
		{Name: "N1", Contents: mobileconfigForTest("N1", "I1")},
		{Name: "N2", Contents: syncMLForTest("./Foo/Bar")},
		{Name: "N4", Contents: declarationForTest("D1")},
	}}, http.StatusNoContent)

	// macOS host, MDM on
	_, macDevice := createHostThenEnrollMDM(s.ds, s.server.URL, t)
	// windows host, MDM on
	createWindowsHostThenEnrollMDM(s.ds, s.server.URL, t)
	// linux and darwin, MDM off
	createOrbitEnrolledHost(t, "linux", "linux_host", s.ds)
	createOrbitEnrolledHost(t, "darwin", "mac_not_enrolled", s.ds)

	// we're going to modify this mock, make sure we restore its default
	originalPushMock := s.pushProvider.PushFunc
	defer func() { s.pushProvider.PushFunc = originalPushMock }()

	var recordedPushes []*mdm.Push
	var mu sync.Mutex
	s.pushProvider.PushFunc = func(ctx context.Context, pushes []*mdm.Push) (map[string]*push.Response, error) {
		mu.Lock()
		defer mu.Unlock()
		recordedPushes = pushes
		return mockSuccessfulPush(ctx, pushes)
	}

	// trigger the reconciliation schedule
	err := ReconcileAppleProfiles(ctx, s.ds, s.mdmCommander, s.logger)
	require.NoError(t, err)
	require.Len(t, recordedPushes, 1)
	recordedPushes = nil

	// triggering the schedule again doesn't send any more pushes
	err = ReconcileAppleProfiles(ctx, s.ds, s.mdmCommander, s.logger)
	require.NoError(t, err)
	require.Len(t, recordedPushes, 0)
	recordedPushes = nil

	// the cron to trigger pushes sends a new push request each time it
	// runs if there are pending commands
	for i := 0; i < 3; i++ {
		err := SendPushesToPendingDevices(ctx, s.ds, s.mdmCommander, s.logger)
		require.NoError(t, err)
		require.Len(t, recordedPushes, 1)
		recordedPushes = nil
	}

	// device acknowledges the commands
	cmd, err := macDevice.Idle()
	require.NoError(t, err)
	require.NotNil(t, cmd)
	for cmd != nil {
		cmd, err = macDevice.Acknowledge(cmd.CommandUUID)
		require.NoError(t, err)
	}

	// no more pushes are enqueued
	err = SendPushesToPendingDevices(ctx, s.ds, s.mdmCommander, s.logger)
	require.NoError(t, err)
	require.Len(t, recordedPushes, 0)
}

func (s *integrationMDMTestSuite) TestAPNsPushWithNotNow() {
	t := s.T()
	ctx := context.Background()

	// macOS host, MDM on
	_, macDevice := createHostThenEnrollMDM(s.ds, s.server.URL, t)
	// windows host, MDM on
	createWindowsHostThenEnrollMDM(s.ds, s.server.URL, t)
	// linux and darwin, MDM off
	createOrbitEnrolledHost(t, "linux", "linux_host", s.ds)
	createOrbitEnrolledHost(t, "darwin", "mac_not_enrolled", s.ds)

	// we're going to modify this mock, make sure we restore its default
	originalPushMock := s.pushProvider.PushFunc
	defer func() { s.pushProvider.PushFunc = originalPushMock }()

	var recordedPushes []*mdm.Push
	var mu sync.Mutex
	s.pushProvider.PushFunc = func(ctx context.Context, pushes []*mdm.Push) (map[string]*push.Response, error) {
		mu.Lock()
		defer mu.Unlock()
		recordedPushes = pushes
		return mockSuccessfulPush(ctx, pushes)
	}

	// trigger the reconciliation schedule
	err := ReconcileAppleProfiles(ctx, s.ds, s.mdmCommander, s.logger)
	require.NoError(t, err)
	require.Len(t, recordedPushes, 1)
	recordedPushes = nil

	// Flush any existing profiles.
	cmd, err := macDevice.Idle()
	require.NoError(t, err)
	for cmd != nil {
		t.Logf("Received: %s %s", cmd.CommandUUID, cmd.Command.RequestType)
		cmd, err = macDevice.Acknowledge(cmd.CommandUUID)
		require.NoError(t, err)
	}

	// Load new profiles
	s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
		{Name: "N1", Contents: mobileconfigForTest("N1", "I1")},
		{Name: "N2", Contents: syncMLForTest("./Foo/Bar")},
	}}, http.StatusNoContent)

	// trigger the reconciliation schedule
	err = ReconcileAppleProfiles(ctx, s.ds, s.mdmCommander, s.logger)
	require.NoError(t, err)
	require.Len(t, recordedPushes, 1)
	recordedPushes = nil

	// The cron to trigger pushes sends a new push request each time it runs.
	err = SendPushesToPendingDevices(ctx, s.ds, s.mdmCommander, s.logger)
	require.NoError(t, err)
	require.Len(t, recordedPushes, 1)
	recordedPushes = nil

	// device sends 'NotNow'
	cmd, err = macDevice.Idle()
	require.NoError(t, err)
	require.NotNil(t, cmd)
	cmd, err = macDevice.NotNow(cmd.CommandUUID)
	require.NoError(t, err)
	assert.Nil(t, cmd)

	// A 'NotNow' command will not trigger a new push. Device is expected to check in again when conditions change.
	err = ReconcileAppleProfiles(ctx, s.ds, s.mdmCommander, s.logger)
	require.NoError(t, err)
	require.Len(t, recordedPushes, 0)
	recordedPushes = nil

	// device acknowledges the commands
	cmd, err = macDevice.Idle()
	require.NoError(t, err)
	require.NotNil(t, cmd)
	cmd, err = macDevice.Acknowledge(cmd.CommandUUID)
	require.NoError(t, err)
	assert.Nil(t, cmd)

	// no more pushes are enqueued
	err = SendPushesToPendingDevices(ctx, s.ds, s.mdmCommander, s.logger)
	require.NoError(t, err)
	assert.Zero(t, recordedPushes)
}

func (s *integrationMDMTestSuite) TestMDMRequestWithoutCerts() {
	t := s.T()
	res := s.DoRawNoAuth("PUT", "/mdm/apple/mdm", nil, http.StatusBadRequest)
	require.NoError(t, res.Body.Close())
}

func (s *integrationMDMTestSuite) TestMDMEnrollDoesntClearLastEnrolledAtForMacOS() {
	t := s.T()

	// Enroll to Fleet with fleetd first.
	desktopToken := uuid.New().String()
	mdmDevice := mdmtest.NewTestMDMClientAppleDesktopManual(s.server.URL, desktopToken)
	fleetHost, err := s.ds.NewHost(context.Background(), &fleet.Host{
		DetailUpdatedAt: time.Now(),
		LabelUpdatedAt:  time.Now(),
		PolicyUpdatedAt: time.Now(),
		LastEnrolledAt:  time.Now(),
		SeenTime:        time.Now().Add(-1 * time.Minute),
		OsqueryHostID:   ptr.String(t.Name() + uuid.New().String()),
		NodeKey:         ptr.String(t.Name() + uuid.New().String()),
		Hostname:        fmt.Sprintf("%sfoo.local", t.Name()),
		Platform:        "darwin",

		UUID:           mdmDevice.UUID,
		HardwareSerial: mdmDevice.SerialNumber,
	})
	require.NoError(t, err)
	err = s.ds.SetOrUpdateDeviceAuthToken(context.Background(), fleetHost.ID, desktopToken)
	require.NoError(t, err)
	hostResp := getHostResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", fleetHost.ID), nil, http.StatusOK, &hostResp)
	require.NotNil(t, hostResp.Host)
	lastEnrolledAt := hostResp.Host.LastEnrolledAt

	assert.Nil(t, hostResp.Host.LastMDMEnrolledAt)
	assert.Nil(t, hostResp.Host.LastMDMCheckedInAt)

	// Add the following here for debug: time.Sleep(2 * time.Second)

	// Enroll with MDM manually after.
	enrollTime := time.Now().UTC().Truncate(time.Second)
	err = mdmDevice.Enroll()
	require.NoError(t, err)

	hostResp = getHostResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", fleetHost.ID), nil, http.StatusOK, &hostResp)
	require.NotNil(t, hostResp.Host)
	assert.Equal(t, lastEnrolledAt, hostResp.Host.LastEnrolledAt)
	assert.NotNil(t, hostResp.Host.LastMDMEnrolledAt)
	assert.GreaterOrEqual(t, *hostResp.Host.LastMDMEnrolledAt, enrollTime)
	assert.NotNil(t, hostResp.Host.LastMDMCheckedInAt)
	assert.GreaterOrEqual(t, *hostResp.Host.LastMDMCheckedInAt, enrollTime)
}

func (s *integrationMDMTestSuite) TestConnectedToFleetWithoutCheckout() {
	t := s.T()
	ctx := context.Background()

	host := createOrbitEnrolledHost(t, "darwin", t.Name(), s.ds)

	// simulate that's actually enrolled to Fleet under the hood
	mdmDevice := mdmtest.NewTestMDMClientAppleDirect(mdmtest.AppleEnrollInfo{
		SCEPChallenge: s.scepChallenge,
		SCEPURL:       s.server.URL + apple_mdm.SCEPPath,
		MDMURL:        s.server.URL + apple_mdm.MDMPath,
	}, "MacBookPro16,1")
	mdmDevice.UUID = host.UUID
	mdmDevice.SerialNumber = host.HardwareSerial
	err := mdmDevice.Enroll()
	require.NoError(t, err)

	// but, set the host as enrolled in a third-party MDM
	err = s.ds.SetOrUpdateMDMData(ctx, host.ID, true, true, "https://foo.com", false, fleet.WellKnownMDMSimpleMDM, "", false)
	require.NoError(t, err)

	// host is connected to Fleet
	var hostResp getHostResponse
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &hostResp)
	require.NotNil(t, hostResp.Host)
	require.NotNil(t, hostResp.Host.MDM.ConnectedToFleet)
	require.True(t, *hostResp.Host.MDM.ConnectedToFleet)

	// now simulate an un-enrollment without checkout, in this case, osquery reports the host as not-enrolled
	err = s.ds.SetOrUpdateMDMData(ctx, host.ID, false, false, "", false, "", "", false)
	require.NoError(t, err)

	// host is not connected to Fleet anymore
	hostResp = getHostResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &hostResp)
	require.NotNil(t, hostResp.Host)
	require.NotNil(t, hostResp.Host.MDM.ConnectedToFleet)
	require.False(t, *hostResp.Host.MDM.ConnectedToFleet)
}

func (s *integrationMDMTestSuite) TestBatchAssociateAppStoreApps() {
	t := s.T()
	batchURL := "/api/latest/fleet/software/app_store_apps/batch"

	// non-existent team
	s.Do("POST", batchURL, batchAssociateAppStoreAppsRequest{}, http.StatusNotFound, "team_name", "foo")

	// create a team
	tmGood, err := s.ds.NewTeam(context.Background(), &fleet.Team{
		Name:        t.Name() + " good",
		Description: "desc",
		Secrets:     []*fleet.EnrollSecret{{Secret: "goodsecret"}},
	})
	require.NoError(t, err)

	// create a team
	tmEmpty, err := s.ds.NewTeam(context.Background(), &fleet.Team{
		Name:        t.Name() + " empty",
		Description: "desc",
	})
	require.NoError(t, err)

	var batchAssociateResponse batchAssociateAppStoreAppsResponse

	// No vpp token set, but request is empty so it succeeds (clears VPP apps for the team).
	s.DoJSON("POST", batchURL, batchAssociateAppStoreAppsRequest{}, http.StatusOK, &batchAssociateResponse, "team_name", tmGood.Name)
	require.Len(t, batchAssociateResponse.Apps, 0)

	// No vpp token set, try association
	// FIXME
	// s.Do("POST", batchURL, batchAssociateAppStoreAppsRequest{Apps: []fleet.VPPBatchPayload{{AppStoreID: s.appleVPPConfigSrvConfig.Assets[0].AdamID}}}, http.StatusUnprocessableEntity, "team_name", tmGood.Name)

	// Valid token
	orgName := "Fleet Device Management Inc."
	token := "mycooltoken"
	expTime := time.Now().Add(200 * time.Hour).UTC().Round(time.Second)
	expDate := expTime.Format(fleet.VPPTimeFormat)
	tokenJSON := fmt.Sprintf(`{"expDate":"%s","token":"%s","orgName":"%s"}`, expDate, token, orgName)
	t.Setenv("FLEET_DEV_VPP_URL", s.appleVPPConfigSrv.URL)
	var vppRes uploadVPPTokenResponse
	s.uploadDataViaForm("/api/latest/fleet/vpp_tokens", "token", "token.vpptoken", []byte(base64.StdEncoding.EncodeToString([]byte(tokenJSON))), http.StatusAccepted, "", &vppRes)

	var resPatchVPP patchVPPTokensTeamsResponse
	s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/vpp_tokens/%d/teams", vppRes.Token.ID), patchVPPTokensTeamsRequest{TeamIDs: []uint{}}, http.StatusOK, &resPatchVPP)

	// Remove all vpp associations from team with no members
	s.DoJSON("POST", batchURL, batchAssociateAppStoreAppsRequest{}, http.StatusOK, &batchAssociateResponse, "team_name", tmGood.Name)
	require.Len(t, batchAssociateResponse.Apps, 0)

	// host with valid serial number
	hValid, err := s.ds.NewHost(context.Background(), &fleet.Host{
		DetailUpdatedAt: time.Now(),
		LabelUpdatedAt:  time.Now(),
		PolicyUpdatedAt: time.Now(),
		HardwareSerial:  "123",
		SeenTime:        time.Now().Add(-1 * time.Minute),
		OsqueryHostID:   ptr.String(t.Name() + uuid.New().String()),
		NodeKey:         ptr.String(t.Name() + uuid.New().String()),
		Hostname:        fmt.Sprintf("%sfoo.local", t.Name()),
		Platform:        "darwin",
	})
	require.NoError(t, err)
	err = s.ds.AddHostsToTeam(context.Background(), fleet.NewAddHostsToTeamParams(&tmGood.ID, []uint{hValid.ID}))
	require.NoError(t, err)

	ctx := context.Background()

	assoc, err := s.ds.GetAssignedVPPApps(ctx, &tmGood.ID)
	require.NoError(t, err)
	require.Len(t, assoc, 0)

	// Remove all vpp associations from team with no members
	s.DoJSON("POST", batchURL, batchAssociateAppStoreAppsRequest{}, http.StatusOK, &batchAssociateResponse, "team_name", tmGood.Name)
	require.Len(t, batchAssociateResponse.Apps, 0)

	// Incorrect type check
	incorrectTypes := struct {
		Apps []struct {
			AppStoreID  int  `json:"app_store_id"`
			SelfService bool `json:"self_service"`
		} `json:"app_store_apps"`
	}{
		Apps: []struct {
			AppStoreID  int  `json:"app_store_id"`
			SelfService bool `json:"self_service"`
		}{
			{
				AppStoreID: 1,
			},
		},
	}
	badTypeReq := s.Do("POST", batchURL, incorrectTypes, http.StatusBadRequest, "team_name", tmGood.Name)
	badTypeBody := extractServerErrorText(badTypeReq.Body)
	assert.Contains(t, badTypeBody, "must be a string")

	// Associating an app we don't own
	s.Do("POST", batchURL, batchAssociateAppStoreAppsRequest{Apps: []fleet.VPPBatchPayload{{AppStoreID: "fake-app"}}}, http.StatusUnprocessableEntity, "team_name", tmGood.Name)

	assoc, err = s.ds.GetAssignedVPPApps(ctx, &tmGood.ID)
	require.NoError(t, err)
	require.Len(t, assoc, 0)

	// Associating an app we own
	s.DoJSON("POST", batchURL, batchAssociateAppStoreAppsRequest{Apps: []fleet.VPPBatchPayload{
		{AppStoreID: s.appleVPPConfigSrvConfig.Assets[0].AdamID},
	}}, http.StatusOK, &batchAssociateResponse, "team_name", tmGood.Name)
	require.Len(t, batchAssociateResponse.Apps, 1)

	assoc, err = s.ds.GetAssignedVPPApps(ctx, &tmGood.ID)
	require.NoError(t, err)
	require.Len(t, assoc, 1)
	assert.Contains(t, assoc, fleet.VPPAppID{AdamID: s.appleVPPConfigSrvConfig.Assets[0].AdamID, Platform: fleet.MacOSPlatform})

	// Associating one good and one bad app
	s.Do("POST",
		batchURL,
		batchAssociateAppStoreAppsRequest{Apps: []fleet.VPPBatchPayload{
			{AppStoreID: s.appleVPPConfigSrvConfig.Assets[0].AdamID},
			{AppStoreID: "fake-app"},
		}}, http.StatusUnprocessableEntity, "team_name", tmGood.Name,
	)

	assoc, err = s.ds.GetAssignedVPPApps(ctx, &tmGood.ID)
	require.NoError(t, err)
	require.Len(t, assoc, 1)

	// Associating two apps we own
	beforeAssociation := time.Now()
	s.DoJSON("POST",
		batchURL,
		batchAssociateAppStoreAppsRequest{
			Apps: []fleet.VPPBatchPayload{
				{AppStoreID: s.appleVPPConfigSrvConfig.Assets[0].AdamID},
				{AppStoreID: s.appleVPPConfigSrvConfig.Assets[1].AdamID, SelfService: true},
			},
		}, http.StatusOK, &batchAssociateResponse, "team_name", tmGood.Name,
	)
	afterAssociation := time.Now()
	require.Len(t, batchAssociateResponse.Apps, 4)
	assoc, err = s.ds.GetAssignedVPPApps(ctx, &tmGood.ID)
	require.NoError(t, err)
	require.Len(t, assoc, 4)
	assert.Contains(t, assoc, fleet.VPPAppID{AdamID: s.appleVPPConfigSrvConfig.Assets[0].AdamID, Platform: fleet.MacOSPlatform})
	assert.Contains(t, assoc, fleet.VPPAppID{AdamID: s.appleVPPConfigSrvConfig.Assets[1].AdamID, Platform: fleet.IOSPlatform})
	assert.Contains(t, assoc, fleet.VPPAppID{AdamID: s.appleVPPConfigSrvConfig.Assets[1].AdamID, Platform: fleet.IPadOSPlatform})
	meta, err := s.ds.GetVPPAppMetadataByAdamIDPlatformTeamID(ctx, s.appleVPPConfigSrvConfig.Assets[1].AdamID, fleet.MacOSPlatform, &tmGood.ID)
	require.NoError(t, err)

	actual := assoc[fleet.VPPAppID{AdamID: s.appleVPPConfigSrvConfig.Assets[1].AdamID, Platform: fleet.MacOSPlatform}]
	// Only macOS version should be self-service
	assert.Equal(t, fleet.VPPAppTeam{
		VPPAppID:           fleet.VPPAppID{AdamID: s.appleVPPConfigSrvConfig.Assets[1].AdamID, Platform: fleet.MacOSPlatform},
		SelfService:        true,
		InstallDuringSetup: ptr.Bool(false),
		AppTeamID:          meta.AppTeamID,
		AddedAt:            actual.AddedAt,
	}, actual)
	require.GreaterOrEqual(t, actual.AddedAt, beforeAssociation)
	require.LessOrEqual(t, actual.AddedAt, afterAssociation)
	require.NotZero(t, actual.AddedAt)

	// Reverse self-service associations
	// Associating two apps we own
	s.DoJSON("POST",
		batchURL,
		batchAssociateAppStoreAppsRequest{
			Apps: []fleet.VPPBatchPayload{
				{AppStoreID: s.appleVPPConfigSrvConfig.Assets[0].AdamID, SelfService: true},
				{AppStoreID: s.appleVPPConfigSrvConfig.Assets[1].AdamID, SelfService: false},
			},
		}, http.StatusOK, &batchAssociateResponse, "team_name", tmGood.Name,
	)
	require.Len(t, batchAssociateResponse.Apps, 4)
	assoc, err = s.ds.GetAssignedVPPApps(ctx, &tmGood.ID)
	require.NoError(t, err)
	require.Len(t, assoc, 4)
	meta, err = s.ds.GetVPPAppMetadataByAdamIDPlatformTeamID(ctx, s.appleVPPConfigSrvConfig.Assets[0].AdamID, fleet.MacOSPlatform, &tmGood.ID)
	require.NoError(t, err)
	actual = assoc[fleet.VPPAppID{AdamID: s.appleVPPConfigSrvConfig.Assets[0].AdamID, Platform: fleet.MacOSPlatform}]
	assert.Equal(t, fleet.VPPAppTeam{
		VPPAppID:           fleet.VPPAppID{AdamID: s.appleVPPConfigSrvConfig.Assets[0].AdamID, Platform: fleet.MacOSPlatform},
		SelfService:        true,
		InstallDuringSetup: ptr.Bool(false),
		AppTeamID:          meta.AppTeamID,
		AddedAt:            actual.AddedAt,
	}, actual)
	meta, err = s.ds.GetVPPAppMetadataByAdamIDPlatformTeamID(ctx, s.appleVPPConfigSrvConfig.Assets[1].AdamID, fleet.IOSPlatform, &tmGood.ID)
	require.NoError(t, err)
	actual = assoc[fleet.VPPAppID{AdamID: s.appleVPPConfigSrvConfig.Assets[1].AdamID, Platform: fleet.IOSPlatform}]
	assert.Equal(t, fleet.VPPAppTeam{
		VPPAppID:           fleet.VPPAppID{AdamID: s.appleVPPConfigSrvConfig.Assets[1].AdamID, Platform: fleet.IOSPlatform},
		InstallDuringSetup: ptr.Bool(false),
		AppTeamID:          meta.AppTeamID,
		AddedAt:            actual.AddedAt,
	}, actual)
	meta, err = s.ds.GetVPPAppMetadataByAdamIDPlatformTeamID(ctx, s.appleVPPConfigSrvConfig.Assets[1].AdamID, fleet.IPadOSPlatform, &tmGood.ID)
	require.NoError(t, err)
	actual = assoc[fleet.VPPAppID{AdamID: s.appleVPPConfigSrvConfig.Assets[1].AdamID, Platform: fleet.IPadOSPlatform}]
	assert.Equal(t, fleet.VPPAppTeam{
		VPPAppID:           fleet.VPPAppID{AdamID: s.appleVPPConfigSrvConfig.Assets[1].AdamID, Platform: fleet.IPadOSPlatform},
		InstallDuringSetup: ptr.Bool(false),
		AppTeamID:          meta.AppTeamID,
		AddedAt:            actual.AddedAt,
	}, actual)
	meta, err = s.ds.GetVPPAppMetadataByAdamIDPlatformTeamID(ctx, s.appleVPPConfigSrvConfig.Assets[1].AdamID, fleet.MacOSPlatform, &tmGood.ID)
	require.NoError(t, err)
	actual = assoc[fleet.VPPAppID{AdamID: s.appleVPPConfigSrvConfig.Assets[1].AdamID, Platform: fleet.MacOSPlatform}]
	assert.Equal(t, fleet.VPPAppTeam{
		VPPAppID:           fleet.VPPAppID{AdamID: s.appleVPPConfigSrvConfig.Assets[1].AdamID, Platform: fleet.MacOSPlatform},
		InstallDuringSetup: ptr.Bool(false),
		AppTeamID:          meta.AppTeamID,
		AddedAt:            actual.AddedAt,
	}, actual)

	// Associate an app with a team with no team members
	s.DoJSON("POST", batchURL, batchAssociateAppStoreAppsRequest{Apps: []fleet.VPPBatchPayload{{AppStoreID: s.appleVPPConfigSrvConfig.Assets[0].AdamID}}}, http.StatusOK, &batchAssociateResponse, "team_name", tmEmpty.Name)

	// Remove all vpp associations
	s.DoJSON("POST", batchURL, batchAssociateAppStoreAppsRequest{}, http.StatusOK, &batchAssociateResponse, "team_name", tmGood.Name)
	require.Len(t, batchAssociateResponse.Apps, 0)

	assoc, err = s.ds.GetAssignedVPPApps(ctx, &tmGood.ID)
	require.NoError(t, err)
	require.Len(t, assoc, 0)

	// Test InstallDuringSetup
	// Helper functions for verifying what's enabled for setup experience
	getReturnedSetupExperienceAdamIDs := func(titles []fleet.SoftwareTitleListResult) []string {
		var adamIDs []string
		for _, title := range titles {
			if (title.AppStoreApp != nil && title.AppStoreApp.InstallDuringSetup != nil && *title.AppStoreApp.InstallDuringSetup == true) ||
				(title.SoftwarePackage != nil && title.SoftwarePackage.InstallDuringSetup != nil && *title.SoftwarePackage.InstallDuringSetup == true) {
				adamIDs = append(adamIDs, title.AppStoreApp.AppStoreID)
			}
		}
		return adamIDs
	}

	checkSetupExperienceVPP := func(t *testing.T, platform string, teamID uint, expectedAdamIDs []string) {
		var respGetSetupExperience getSetupExperienceSoftwareResponse
		s.DoJSON("GET", "/api/latest/fleet/setup_experience/software", getSetupExperienceSoftwareRequest{},
			http.StatusOK,
			&respGetSetupExperience,
			"platform", platform,
			"team_id", fmt.Sprint(teamID),
		)
		assert.ElementsMatch(t, getReturnedSetupExperienceAdamIDs(respGetSetupExperience.SoftwareTitles), expectedAdamIDs)
	}

	// Associate with the SetupExperience flag set to true
	s.DoJSON("POST",
		batchURL,
		batchAssociateAppStoreAppsRequest{
			Apps: []fleet.VPPBatchPayload{
				// macOS only
				{AppStoreID: s.appleVPPConfigSrvConfig.Assets[0].AdamID, InstallDuringSetup: ptr.Bool(true)},
				// macOS, iOS, iPadOS
				{AppStoreID: s.appleVPPConfigSrvConfig.Assets[1].AdamID, SelfService: true, InstallDuringSetup: ptr.Bool(true)},
				// iPadOS only
				{AppStoreID: s.appleVPPConfigSrvConfig.Assets[2].AdamID, SelfService: true, InstallDuringSetup: ptr.Bool(true)},
			},
		}, http.StatusOK, &batchAssociateResponse, "team_name", tmGood.Name,
	)

	checkSetupExperienceVPP(t, "macos", tmGood.ID, []string{s.appleVPPConfigSrvConfig.Assets[0].AdamID, s.appleVPPConfigSrvConfig.Assets[1].AdamID})
	checkSetupExperienceVPP(t, string(fleet.IPadOSPlatform), tmGood.ID, []string{s.appleVPPConfigSrvConfig.Assets[1].AdamID, s.appleVPPConfigSrvConfig.Assets[2].AdamID})
	checkSetupExperienceVPP(t, string(fleet.IOSPlatform), tmGood.ID, []string{s.appleVPPConfigSrvConfig.Assets[1].AdamID})

	// Associate with the SetupExperience flag set to nil = no change
	s.DoJSON("POST",
		batchURL,
		batchAssociateAppStoreAppsRequest{
			Apps: []fleet.VPPBatchPayload{
				// macOS only
				{AppStoreID: s.appleVPPConfigSrvConfig.Assets[0].AdamID, InstallDuringSetup: nil},
				// macOS, iOS, iPadOS
				{AppStoreID: s.appleVPPConfigSrvConfig.Assets[1].AdamID, SelfService: true, InstallDuringSetup: nil},
				// iPadOS only
				{AppStoreID: s.appleVPPConfigSrvConfig.Assets[2].AdamID, SelfService: true, InstallDuringSetup: nil},
			},
		}, http.StatusOK, &batchAssociateResponse, "team_name", tmGood.Name,
	)

	checkSetupExperienceVPP(t, "macos", tmGood.ID, []string{s.appleVPPConfigSrvConfig.Assets[0].AdamID, s.appleVPPConfigSrvConfig.Assets[1].AdamID})
	checkSetupExperienceVPP(t, string(fleet.IPadOSPlatform), tmGood.ID, []string{s.appleVPPConfigSrvConfig.Assets[1].AdamID, s.appleVPPConfigSrvConfig.Assets[2].AdamID})
	checkSetupExperienceVPP(t, string(fleet.IOSPlatform), tmGood.ID, []string{s.appleVPPConfigSrvConfig.Assets[1].AdamID})

	// Associate with the SetupExperience flag set to false
	s.DoJSON("POST",
		batchURL,
		batchAssociateAppStoreAppsRequest{
			Apps: []fleet.VPPBatchPayload{
				// macOS only
				{AppStoreID: s.appleVPPConfigSrvConfig.Assets[0].AdamID, InstallDuringSetup: ptr.Bool(false)},
				// macOS, iOS, iPadOS
				{AppStoreID: s.appleVPPConfigSrvConfig.Assets[1].AdamID, SelfService: true, InstallDuringSetup: ptr.Bool(false)},
				// iPadOS only
				{AppStoreID: s.appleVPPConfigSrvConfig.Assets[2].AdamID, SelfService: true, InstallDuringSetup: ptr.Bool(false)},
			},
		}, http.StatusOK, &batchAssociateResponse, "team_name", tmGood.Name,
	)

	checkSetupExperienceVPP(t, "macos", tmGood.ID, []string{})
	checkSetupExperienceVPP(t, string(fleet.IPadOSPlatform), tmGood.ID, []string{})
	checkSetupExperienceVPP(t, string(fleet.IOSPlatform), tmGood.ID, []string{})

	// ===================================================
	//                   Android apps
	// ===================================================

	// Android MDM not set up yet, should fail
	resp := s.Do("POST", batchURL, batchAssociateAppStoreAppsRequest{Apps: []fleet.VPPBatchPayload{
		{AppStoreID: s.appleVPPConfigSrvConfig.Assets[0].AdamID}, {AppStoreID: "com.foo.bar", Platform: fleet.AndroidPlatform},
	}}, http.StatusBadRequest, "team_name", tmGood.Name)

	s.Assert().Contains(extractServerErrorText(resp.Body), "Android MDM is not enabled")

	s.enableAndroidMDM(s.T())

	// Android app not found, should fail
	s.androidAPIClient.EnterprisesApplicationsFunc = func(ctx context.Context, enterpriseName string, packageName string) (*androidmanagement.Application, error) {
		return nil, &notFoundError{}
	}
	resp = s.Do("POST", batchURL, batchAssociateAppStoreAppsRequest{Apps: []fleet.VPPBatchPayload{
		{AppStoreID: s.appleVPPConfigSrvConfig.Assets[0].AdamID}, {AppStoreID: "com.app.not.found", Platform: fleet.AndroidPlatform},
	}}, http.StatusUnprocessableEntity, "team_name", tmGood.Name)

	s.Assert().Contains(extractServerErrorText(resp.Body), "Validation Failed: Couldn't add software. The application ID isn't available in Play Store. Please find ID on the Play Store and try again.")

	s.androidAPIClient.EnterprisesApplicationsFunc = func(ctx context.Context, enterpriseName string, packageName string) (*androidmanagement.Application, error) {
		return &androidmanagement.Application{}, nil
	}

	// Enroll an Android host
	secrets, err := s.ds.GetEnrollSecrets(ctx, &tmGood.ID)
	require.NoError(t, err)
	require.Len(t, secrets, 1)

	assets, err := s.ds.GetAllMDMConfigAssetsByName(ctx, []fleet.MDMAssetName{fleet.MDMAssetAndroidPubSubToken}, nil)
	require.NoError(t, err)
	pubsubToken := assets[fleet.MDMAssetAndroidPubSubToken]
	require.NotEmpty(t, pubsubToken.Value)

	deviceID1 := createAndroidDeviceID("test-android")

	enterpriseSpecificID1 := strings.ToUpper(uuid.New().String())
	var req android_service.PubSubPushRequest
	for _, d := range []struct {
		id  string
		esi string
	}{{deviceID1, enterpriseSpecificID1}} {
		enrollmentMessage := enrollmentMessageWithEnterpriseSpecificID(
			t,
			androidmanagement.Device{
				Name:                d.id,
				EnrollmentTokenData: fmt.Sprintf(`{"EnrollSecret": "%s"}`, secrets[0].Secret),
			},
			d.esi,
		)

		req = android_service.PubSubPushRequest{
			PubSubMessage: *enrollmentMessage,
		}

		s.Do("POST", "/api/v1/fleet/android_enterprise/pubsub", &req, http.StatusOK, "token", string(pubsubToken.Value))
	}

	var hosts listHostsResponse
	s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &hosts, "query", enterpriseSpecificID1)

	assert.Len(t, hosts.Hosts, 1)

	// Run setup experience job
	s.runWorker()
	time.Sleep(time.Second)

	chromeAppID := "com.google.chrome"
	driveAppID := "com.google.drive"
	s.DoJSON("POST", batchURL, batchAssociateAppStoreAppsRequest{Apps: []fleet.VPPBatchPayload{
		{AppStoreID: s.appleVPPConfigSrvConfig.Assets[0].AdamID},
		{AppStoreID: chromeAppID, Platform: fleet.AndroidPlatform},
		{AppStoreID: driveAppID, Platform: fleet.AndroidPlatform},
	}}, http.StatusOK, &batchAssociateResponse, "team_name", tmGood.Name)
	require.Len(t, batchAssociateResponse.Apps, 3)

	assoc, err = s.ds.GetAssignedVPPApps(ctx, &tmGood.ID)
	require.NoError(t, err)
	require.Len(t, assoc, 3)

	for _, a := range []fleet.VPPAppID{
		{AdamID: s.appleVPPConfigSrvConfig.Assets[0].AdamID, Platform: fleet.MacOSPlatform},
		{AdamID: chromeAppID, Platform: fleet.AndroidPlatform},
		{AdamID: driveAppID, Platform: fleet.AndroidPlatform},
	} {
		assert.Contains(t, assoc, a)
	}

	s.androidAPIClient.EnterprisesPoliciesPatchFuncInvoked = false

	checkJobs := func(expectedAppIDs []string) {
		mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
			w := &worker.SoftwareWorker{}
			var j fleet.Job

			err := sqlx.GetContext(ctx, q, &j, "SELECT * FROM jobs WHERE name = ? ORDER BY updated_at DESC LIMIT 1", w.Name())
			s.Require().NoError(err)

			var args map[string]any
			s.Require().NoError(json.Unmarshal(*j.Args, &args))
			s.Assert().Equal("bulk_set_android_apps_available_for_host", args["task"])
			s.Assert().Equal(fleet.JobStateSuccess, j.State)
			s.Assert().ElementsMatch(expectedAppIDs, args["application_ids"])

			return nil
		})

	}

	// Run worker to set apps available to device
	time.Sleep(time.Second)
	s.runWorker()
	s.Assert().True(s.androidAPIClient.EnterprisesPoliciesPatchFuncInvoked)
	s.androidAPIClient.EnterprisesPoliciesPatchFuncInvoked = false
	checkJobs([]string{chromeAppID, driveAppID})

	// Remove an app
	s.DoJSON("POST", batchURL, batchAssociateAppStoreAppsRequest{Apps: []fleet.VPPBatchPayload{
		{AppStoreID: s.appleVPPConfigSrvConfig.Assets[0].AdamID},
		{AppStoreID: driveAppID, Platform: fleet.AndroidPlatform},
	}}, http.StatusOK, &batchAssociateResponse, "team_name", tmGood.Name)
	require.Len(t, batchAssociateResponse.Apps, 2)

	assoc, err = s.ds.GetAssignedVPPApps(ctx, &tmGood.ID)
	require.NoError(t, err)
	require.Len(t, assoc, 2)

	for _, a := range []fleet.VPPAppID{
		{AdamID: s.appleVPPConfigSrvConfig.Assets[0].AdamID, Platform: fleet.MacOSPlatform},
		{AdamID: driveAppID, Platform: fleet.AndroidPlatform},
	} {
		assert.Contains(t, assoc, a)
	}

	time.Sleep(time.Second)
	s.runWorker()
	s.Assert().True(s.androidAPIClient.EnterprisesPoliciesPatchFuncInvoked)
	checkJobs([]string{driveAppID})
}

func (s *integrationMDMTestSuite) TestInvalidCommandUUID() {
	t := s.T()
	_, device := createHostThenEnrollMDM(s.ds, s.server.URL, t)
	s.runWorker()
	cmd, err := device.Acknowledge("foo")
	require.NoError(t, err)
	require.NotNil(t, cmd)
}

func (s *integrationMDMTestSuite) TestEnrollAfterDEPSyncIOSIPadOS() {
	t := s.T()

	h, _ := s.createAppleMobileHostThenEnrollMDM("ios")

	// fetch the host, it will match the one created above
	// (NOTE: cannot check the returned OrbitNodeKey, this field is not part of the response)
	var hostResp getHostResponse
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", h.ID), nil, http.StatusOK, &hostResp)
	require.Equal(t, h.ID, hostResp.Host.ID)
	require.NotEqual(t, h.LastEnrolledAt, hostResp.Host.LastEnrolledAt)

	h, _ = s.createAppleMobileHostThenEnrollMDM("ipados")

	// fetch the host, it will match the one created above
	// (NOTE: cannot check the returned OrbitNodeKey, this field is not part of the response)
	hostResp = getHostResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", h.ID), nil, http.StatusOK, &hostResp)
	require.Equal(t, h.ID, hostResp.Host.ID)
	require.NotEqual(t, h.LastEnrolledAt, hostResp.Host.LastEnrolledAt)

	// list commands returns empty set -- no MDM commands should be queued at this point
	var listCmdResp listMDMAppleCommandsResponse
	s.DoJSON("GET", "/api/latest/fleet/mdm/apple/commands", nil, http.StatusOK, &listCmdResp)
	require.Empty(t, listCmdResp.Results)
}

func (s *integrationMDMTestSuite) TestRefetchIOSIPadOS() {
	t := s.T()

	testCerts := []*x509.Certificate{mdmtesting.NewTestMDMAppleCertTemplate()}

	// Try to refetch host that is not MDM enrolled
	serialNumber := mdmtest.RandSerialNumber()
	fleetHost, err := s.ds.NewHost(context.Background(), &fleet.Host{
		HardwareSerial:   serialNumber,
		Platform:         "ipados",
		LastEnrolledAt:   time.Now(),
		DetailUpdatedAt:  time.Now(),
		RefetchRequested: true,
	})
	require.NoError(t, err)
	r := s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/refetch", fleetHost.ID), nil, http.StatusUnprocessableEntity, "error",
		"host is not enrolled in MDM")
	assert.Contains(t, extractServerErrorText(r.Body), "Host does not have MDM turned on")

	// Enroll host
	host, mdmClient := s.createAppleMobileHostThenEnrollMDM("ios")
	require.NoError(t, s.ds.SetOrUpdateMDMData(context.Background(), host.ID, false, true, "https://foo.com", true, "", "", false))

	// Refetch host
	_ = s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/refetch", host.ID), nil, http.StatusOK)
	const commandsSentPerRefetch = 3
	commandsSent := commandsSentPerRefetch

	var hostResp getHostResponse
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &hostResp)
	assert.Equal(t, host.ID, hostResp.Host.ID)
	assert.True(t, hostResp.Host.RefetchRequested)

	commands, err := s.ds.GetHostMDMCommands(context.Background(), host.ID)
	require.NoError(t, err)
	require.Len(t, commands, commandsSent)
	assert.ElementsMatch(t, []fleet.HostMDMCommand{
		{HostID: host.ID, CommandType: fleet.RefetchAppsCommandUUIDPrefix},
		{HostID: host.ID, CommandType: fleet.RefetchCertsCommandUUIDPrefix},
		{HostID: host.ID, CommandType: fleet.RefetchDeviceCommandUUIDPrefix},
	}, commands)

	// Since refetch is already queued up, doing another refetch is a no-op and will not add more MDM commands
	_ = s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/refetch", host.ID), nil, http.StatusOK)

	// Check the MDM commands and send response
	cmd, err := mdmClient.Idle()
	require.NoError(t, err)
	require.NotNil(t, cmd)

	const deviceName = "My iPhone"
	expectedSoftware := []fleet.HostSoftwareEntry{
		{
			Software: fleet.Software{
				BundleIdentifier: "com.evernote.iPhone.Evernote",
				Name:             "Evernote",
				Version:          "10.98.0",
				Source:           "ios_apps",
			},
		},
	}
	require.Equal(t, "InstalledApplicationList", cmd.Command.RequestType)
	cmd, err = mdmClient.AcknowledgeInstalledApplicationList(mdmClient.UUID, cmd.CommandUUID,
		[]fleet.Software{expectedSoftware[0].Software})
	require.NoError(t, err)
	require.Equal(t, "CertificateList", cmd.Command.RequestType)
	cmd, err = mdmClient.AcknowledgeCertificateList(mdmClient.UUID, cmd.CommandUUID, testCerts)
	require.NoError(t, err)
	require.Equal(t, "DeviceInformation", cmd.Command.RequestType)
	_, err = mdmClient.AcknowledgeDeviceInformation(mdmClient.UUID, cmd.CommandUUID, deviceName, "iPhone SE")
	require.NoError(t, err)

	commands, err = s.ds.GetHostMDMCommands(context.Background(), host.ID)
	require.NoError(t, err)
	require.Empty(t, commands)

	hostResp = getHostResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &hostResp)
	assert.Equal(t, host.ID, hostResp.Host.ID)
	assert.False(t, hostResp.Host.RefetchRequested)
	assert.Equal(t, deviceName, hostResp.Host.ComputerName)

	for index := range hostResp.Host.Software {
		hostResp.Host.Software[index].ID = 0
	}
	assert.ElementsMatch(t, expectedSoftware, hostResp.Host.Software)

	// TODO: add test for GET /hosts/:id/certificates endpoint, should match up with testCerts

	// Install the same app for iPadOS
	hostIPad, mdmClientIPad := s.createAppleMobileHostThenEnrollMDM("ipados")

	// Refetch host
	_ = s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/refetch", hostIPad.ID), nil, http.StatusOK)
	commandsSent += commandsSentPerRefetch

	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", hostIPad.ID), nil, http.StatusOK, &hostResp)
	assert.Equal(t, hostIPad.ID, hostResp.Host.ID)
	assert.True(t, hostResp.Host.RefetchRequested)

	// Check the MDM commands and send response
	cmd, err = mdmClientIPad.Idle()
	require.NoError(t, err)
	require.NotNil(t, cmd)

	const deviceNameIPad = "My iPad"
	expectedSoftware = []fleet.HostSoftwareEntry{
		{
			Software: fleet.Software{
				BundleIdentifier: "com.evernote.iPhone.Evernote",
				Name:             "Evernote",
				Version:          "10.98.0",
				Source:           "ipados_apps",
			},
		},
	}
	require.Equal(t, "InstalledApplicationList", cmd.Command.RequestType)
	cmd, err = mdmClientIPad.AcknowledgeInstalledApplicationList(mdmClientIPad.UUID, cmd.CommandUUID,
		[]fleet.Software{expectedSoftware[0].Software})
	require.NoError(t, err)
	require.Equal(t, "CertificateList", cmd.Command.RequestType)
	cmd, err = mdmClientIPad.AcknowledgeCertificateList(mdmClientIPad.UUID, cmd.CommandUUID, testCerts)
	require.NoError(t, err)
	require.Equal(t, "DeviceInformation", cmd.Command.RequestType)
	cmd, err = mdmClientIPad.AcknowledgeDeviceInformation(mdmClientIPad.UUID, cmd.CommandUUID, deviceNameIPad, "iPad 10")
	require.NoError(t, err)
	require.Nil(t, cmd)

	hostResp = getHostResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", hostIPad.ID), nil, http.StatusOK, &hostResp)
	assert.Equal(t, hostIPad.ID, hostResp.Host.ID)
	assert.False(t, hostResp.Host.RefetchRequested)
	assert.Equal(t, deviceNameIPad, hostResp.Host.ComputerName)

	for index := range hostResp.Host.Software {
		hostResp.Host.Software[index].ID = 0
	}
	assert.ElementsMatch(t, expectedSoftware, hostResp.Host.Software)

	// TODO: add test for GET /hosts/:id/certificates endpoint, should match up with testCerts

	hostsCountTs := time.Now().UTC()
	require.NoError(t, s.ds.SyncHostsSoftware(context.Background(), hostsCountTs))
	ctx := context.Background()
	require.NoError(t, s.ds.SyncHostsSoftwareTitles(ctx, hostsCountTs))

	// Check that we have the correct software titles
	var resp listSoftwareTitlesResponse
	s.DoJSON("GET", "/api/latest/fleet/software/titles", nil, http.StatusOK, &resp, "query", "Evernote")
	expectedTitles := []fleet.SoftwareTitleListResult{
		{
			BundleIdentifier: ptr.String("com.evernote.iPhone.Evernote"),
			Name:             "Evernote",
			Source:           "ios_apps",
			HostsCount:       1,
			VersionsCount:    1,
		},
		{
			BundleIdentifier: ptr.String("com.evernote.iPhone.Evernote"),
			Name:             "Evernote",
			Source:           "ipados_apps",
			HostsCount:       1,
			VersionsCount:    1,
		},
	}
	require.Len(t, resp.SoftwareTitles, 2)
	// Cleaning up the response to make it easier to compare using ElementsMatch
	for index := range resp.SoftwareTitles {
		resp.SoftwareTitles[index].ID = 0
		assert.Len(t, resp.SoftwareTitles[index].Versions, 1)
		resp.SoftwareTitles[index].Versions = nil
	}
	assert.ElementsMatch(t, expectedTitles, resp.SoftwareTitles)

	// Test that software was deleted from device
	_ = s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/refetch", host.ID), nil, http.StatusOK)
	commandsSent += commandsSentPerRefetch

	// Check the MDM commands and send response
	cmd, err = mdmClient.Idle()
	require.NoError(t, err)
	require.NotNil(t, cmd)
	require.Equal(t, "InstalledApplicationList", cmd.Command.RequestType)
	cmd, err = mdmClient.AcknowledgeInstalledApplicationList(mdmClient.UUID, cmd.CommandUUID, []fleet.Software{})
	require.NoError(t, err)
	require.Equal(t, "CertificateList", cmd.Command.RequestType)
	cmd, err = mdmClient.AcknowledgeCertificateList(mdmClient.UUID, cmd.CommandUUID, []*x509.Certificate{})
	require.NoError(t, err)
	require.Equal(t, "DeviceInformation", cmd.Command.RequestType)
	const deviceNameRenamed = "My new iPhone"
	cmd, err = mdmClient.AcknowledgeDeviceInformation(mdmClient.UUID, cmd.CommandUUID, deviceNameRenamed, "iPhone SE")
	require.NoError(t, err)
	require.Nil(t, cmd)

	hostResp = getHostResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &hostResp)
	assert.Equal(t, host.ID, hostResp.Host.ID)
	assert.False(t, hostResp.Host.RefetchRequested)
	assert.Equal(t, deviceNameRenamed, hostResp.Host.ComputerName)
	assert.Empty(t, hostResp.Host.Software)

	// TODO: add test for GET /hosts/:id/certificates endpoint, should be empty

	// Mark host as unenrolled and refetch.
	require.NoError(t, s.ds.UpdateMDMData(ctx, host.ID, false))
	hostResp = getHostResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &hostResp)
	require.NoError(t, err)
	require.NotNil(t, hostResp.Host.MDM.EnrollmentStatus)
	assert.Equal(t, "Pending", *hostResp.Host.MDM.EnrollmentStatus)

	// Set iOS detail_updated_at as 2 hours in the past.
	mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
		_, err := q.ExecContext(ctx, `UPDATE hosts SET detail_updated_at = DATE_SUB(NOW(), INTERVAL 2 HOUR) WHERE id = ?`, host.ID)
		return err
	})
	trigger := triggerRequest{
		Name: string(fleet.CronAppleMDMIPhoneIPadRefetcher),
	}
	_ = s.Do("POST", "/api/latest/fleet/trigger", trigger, http.StatusOK)
	commandsSent += commandsSentPerRefetch

	// Wait until MDM commands are set up
	done := make(chan struct{})
	go func() {
		ticker := time.NewTicker(100 * time.Millisecond)
		defer ticker.Stop()
		for range ticker.C {
			commands, err = s.ds.GetHostMDMCommands(context.Background(), host.ID)
			require.NoError(t, err)
			if len(commands) == commandsSentPerRefetch {
				done <- struct{}{}
				return
			}
		}
	}()
	select {
	case <-done:
	case <-time.After(10 * time.Second):
		t.Error("Timeout: MDM commands not queued up")
	}

	// Check the MDM commands and send response
	cmd, err = mdmClient.Idle()
	require.NoError(t, err)
	require.NotNil(t, cmd)
	require.Equal(t, "InstalledApplicationList", cmd.Command.RequestType)
	cmd, err = mdmClient.AcknowledgeInstalledApplicationList(mdmClient.UUID, cmd.CommandUUID, []fleet.Software{})
	require.NoError(t, err)
	require.Equal(t, "CertificateList", cmd.Command.RequestType)
	cmd, err = mdmClient.AcknowledgeCertificateList(mdmClient.UUID, cmd.CommandUUID, []*x509.Certificate{})
	require.Equal(t, "DeviceInformation", cmd.Command.RequestType)
	cmd, err = mdmClient.AcknowledgeDeviceInformation(mdmClient.UUID, cmd.CommandUUID, deviceNameRenamed, "iPhone SE")
	require.NoError(t, err)
	require.Nil(t, cmd)

	hostResp = getHostResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &hostResp)
	assert.Equal(t, host.ID, hostResp.Host.ID)
	assert.False(t, hostResp.Host.RefetchRequested)
	require.NotNil(t, hostResp.Host.MDM.EnrollmentStatus)
	assert.Equal(t, "On (automatic)", *hostResp.Host.MDM.EnrollmentStatus)

	// TODO: add test for GET /hosts/:id/certificates endpoint, should be empty

	// list commands should return all the commands we sent
	var listCmdResp listMDMAppleCommandsResponse
	s.DoJSON("GET", "/api/latest/fleet/mdm/apple/commands", nil, http.StatusOK, &listCmdResp)
	require.Len(t, listCmdResp.Results, commandsSent)
}

func checkInstallFleetdCommandSent(t *testing.T, mdmDevice *mdmtest.TestAppleMDMClient, wantCommand bool) {
	foundInstallFleetdCommand := false
	cmd, err := mdmDevice.Idle()
	require.NoError(t, err)
	for cmd != nil {
		var fullCmd micromdm.CommandPayload
		require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd))
		if manifest := fullCmd.Command.InstallEnterpriseApplication.ManifestURL; manifest != nil {
			foundInstallFleetdCommand = true
			require.Equal(t, "InstallEnterpriseApplication", cmd.Command.RequestType)
			require.Contains(t, *fullCmd.Command.InstallEnterpriseApplication.ManifestURL, fleetdbase.GetPKGManifestURL())
		}
		cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID)
		require.NoError(t, err)
	}
	require.Equal(t, wantCommand, foundInstallFleetdCommand)
}

func (s *integrationMDMTestSuite) TestVPPApps() {
	t := s.T()
	s.setSkipWorkerJobs(t)

	// Invalid token
	t.Setenv("FLEET_DEV_VPP_URL", s.appleVPPConfigSrv.URL+"?invalidToken")
	s.uploadDataViaForm("/api/latest/fleet/vpp_tokens", "token", "token.vpptoken", []byte("foobar"), http.StatusUnprocessableEntity, "Invalid token. Please provide a valid content token from Apple Business Manager.", nil)
	// Attempt to renew an invalid (nonexistent) token, should fail
	s.uploadDataViaFormWithVerb("/api/latest/fleet/vpp_tokens/999/renew", "PATCH", "token", "token.vpptoken", []byte(base64.StdEncoding.EncodeToString([]byte("foobar"))), http.StatusUnprocessableEntity, "Invalid token. Please provide a valid content token from Apple Business Manager.", nil)

	// Simulate a server error from the Apple API
	t.Setenv("FLEET_DEV_VPP_URL", s.appleVPPConfigSrv.URL+"?serverError")
	s.uploadDataViaForm("/api/latest/fleet/vpp_tokens", "token", "token.vpptoken", []byte("foobar"), http.StatusInternalServerError, "Apple VPP endpoint returned error: Internal server error (error number: 9603)", nil)

	// Valid token
	orgName := "Fleet Device Management Inc."
	location := "Fleet Location One"
	token := "mycooltoken"
	expTime := time.Now().Add(200 * time.Hour).UTC().Round(time.Second)
	expDate := expTime.Format(fleet.VPPTimeFormat)
	tokenJSON := fmt.Sprintf(`{"expDate":"%s","token":"%s","orgName":"%s"}`, expDate, token, orgName)
	t.Setenv("FLEET_DEV_VPP_URL", s.appleVPPConfigSrv.URL)
	var validToken uploadVPPTokenResponse
	s.uploadDataViaForm("/api/latest/fleet/vpp_tokens", "token", "token.vpptoken", []byte(base64.StdEncoding.EncodeToString([]byte(tokenJSON))), http.StatusAccepted, "", &validToken)

	s.lastActivityMatches(fleet.ActivityEnabledVPP{}.ActivityName(), "", 0)

	// Renew valid token
	expTime = expTime.Add(24 * time.Hour).UTC().Round(time.Second)
	expDate = expTime.Format(fleet.VPPTimeFormat)
	tokenJSON = fmt.Sprintf(`{"expDate":"%s","token":"%s","orgName":"%s"}`, expDate, token, orgName)
	s.uploadDataViaFormWithVerb(fmt.Sprintf("/api/latest/fleet/vpp_tokens/%d/renew", validToken.Token.ID), "PATCH", "token", "token.vpptoken", []byte(base64.StdEncoding.EncodeToString([]byte(tokenJSON))), http.StatusAccepted, "", &validToken)
	// Valid token bytes, but non-existent token ID. Should get not found
	s.uploadDataViaFormWithVerb("/api/latest/fleet/vpp_tokens/999/renew", "PATCH", "token", "token.vpptoken", []byte(base64.StdEncoding.EncodeToString([]byte(tokenJSON))), http.StatusNotFound, "VPPToken was not found in the datastore", nil)

	// Get the token
	var resp getVPPTokensResponse
	s.DoJSON("GET", "/api/latest/fleet/vpp_tokens", &getVPPTokensRequest{}, http.StatusOK, &resp)
	require.NoError(t, resp.Err)
	require.Len(t, resp.Tokens, 1)
	require.Equal(t, orgName, resp.Tokens[0].OrgName)
	require.Equal(t, location, resp.Tokens[0].Location)
	require.Equal(t, expTime, resp.Tokens[0].RenewDate)

	getSoftwareTitleIDFromApp := func(app *fleet.VPPApp) uint {
		var titleID uint
		mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
			ctx := context.Background()
			return sqlx.GetContext(ctx, q, &titleID, `SELECT title_id FROM vpp_apps WHERE adam_id = ? AND platform = ?`, app.AdamID, app.Platform)
		})

		return titleID
	}

	t.Run("vpp apps with labels", func(t *testing.T) {
		// Create a team
		var newTeamResp teamResponse
		s.DoJSON("POST", "/api/latest/fleet/teams", &createTeamRequest{TeamPayload: fleet.TeamPayload{Name: ptr.String("Team Labels" + t.Name())}}, http.StatusOK, &newTeamResp)
		team := newTeamResp.Team

		// Add an MDM macOS host to the team
		host, mdmDevice := createHostThenEnrollMDM(s.ds, s.server.URL, t)
		orbitKey := setOrbitEnrollment(t, host, s.ds)
		host.OrbitNodeKey = &orbitKey
		s.Do("POST", "/api/latest/fleet/hosts/transfer", &addHostsToTeamRequest{HostIDs: []uint{host.ID}, TeamID: &team.ID}, http.StatusOK)

		// Associate team to the VPP token.
		var resPatchVPP patchVPPTokensTeamsResponse
		s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/vpp_tokens/%d/teams", resp.Tokens[0].ID), patchVPPTokensTeamsRequest{TeamIDs: []uint{team.ID}}, http.StatusOK, &resPatchVPP)

		var createLabelResp createLabelResponse
		s.DoJSON("POST", "/api/latest/fleet/labels", &fleet.LabelPayload{Name: "label1" + t.Name()}, http.StatusOK, &createLabelResp)
		l1 := createLabelResp.Label
		require.NotNil(t, l1)

		s.DoJSON("POST", "/api/latest/fleet/labels", &fleet.LabelPayload{Name: "label2" + t.Name()}, http.StatusOK, &createLabelResp)
		l2 := createLabelResp.Label
		require.NotNil(t, l2)

		includeAnyApp := fleet.VPPApp{
			VPPAppTeam: fleet.VPPAppTeam{
				VPPAppID: fleet.VPPAppID{
					AdamID:   "1",
					Platform: fleet.MacOSPlatform,
				},
			},
			Name:             "App 1",
			BundleIdentifier: "a-1",
			IconURL:          "https://example.com/images/1",
			LatestVersion:    "1.0.0",
		}

		// Attempt to add an app with both types of labels. Should fail
		var addAppResp addAppStoreAppResponse
		addAppReq := &addAppStoreAppRequest{
			TeamID:           &team.ID,
			AppStoreID:       includeAnyApp.AdamID,
			SelfService:      true,
			LabelsIncludeAny: []string{l1.Name},
			LabelsExcludeAny: []string{l2.Name},
		}
		res := s.Do("POST", "/api/latest/fleet/software/app_store_apps", addAppReq, http.StatusBadRequest)
		require.Contains(t, extractServerErrorText(res.Body), `Only one of "labels_include_any" or "labels_exclude_any" can be included`)

		// Now add it for real
		addAppReq.LabelsExcludeAny = []string{}
		s.DoJSON("POST", "/api/latest/fleet/software/app_store_apps", addAppReq, http.StatusOK, &addAppResp)
		titleID := getSoftwareTitleIDFromApp(&includeAnyApp)
		require.Equal(t, titleID, addAppResp.TitleID, "addAppResp should contain the correct software title ID")
		activityData := `{"team_name": "%s", "software_title": "%s", "software_title_id": %d, "app_store_id": "%s", "team_id": %d, "platform": "%s", "self_service": true, "labels_include_any": [{"id": %d, "name": %q}]}`
		s.lastActivityMatches(fleet.ActivityAddedAppStoreApp{}.ActivityName(),
			fmt.Sprintf(activityData, team.Name,
				includeAnyApp.Name, titleID, includeAnyApp.AdamID, team.ID, includeAnyApp.Platform, l1.ID, l1.Name), 0)

		var getSWTitle getSoftwareTitleResponse
		s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d", titleID), nil, http.StatusOK, &getSWTitle, "team_id", fmt.Sprint(team.ID))
		require.NotNil(t, getSWTitle.SoftwareTitle.AppStoreApp)
		require.Equal(t, getSWTitle.SoftwareTitle.AppStoreApp.AdamID, includeAnyApp.AdamID)
		require.Empty(t, getSWTitle.SoftwareTitle.AppStoreApp.LabelsExcludeAny)
		require.Equal(t, getSWTitle.SoftwareTitle.AppStoreApp.LabelsIncludeAny, []fleet.SoftwareScopeLabel{{LabelName: l1.Name, LabelID: l1.ID}})

		// Add an app with exclude_any labels
		excludeAnyApp := fleet.VPPApp{
			VPPAppTeam: fleet.VPPAppTeam{
				VPPAppID: fleet.VPPAppID{
					AdamID:   "2",
					Platform: fleet.MacOSPlatform,
				},
			},
			Name:             "App 2",
			BundleIdentifier: "b-2",
			IconURL:          "https://example.com/images/1",
			LatestVersion:    "1.0.0",
		}

		addAppReq = &addAppStoreAppRequest{
			TeamID:           &team.ID,
			AppStoreID:       excludeAnyApp.AdamID,
			SelfService:      true,
			LabelsExcludeAny: []string{l2.Name},
		}
		s.DoJSON("POST", "/api/latest/fleet/software/app_store_apps", addAppReq, http.StatusOK, &addAppResp)
		titleID = getSoftwareTitleIDFromApp(&excludeAnyApp)
		activityData = `{"team_name": "%s", "software_title": "%s", "software_title_id": %d, "app_store_id": "%s", "team_id": %d, "platform": "%s", "self_service": true, "labels_exclude_any": [{"id": %d, "name": %q}]}`
		s.lastActivityMatches(fleet.ActivityAddedAppStoreApp{}.ActivityName(),
			fmt.Sprintf(activityData, team.Name,
				excludeAnyApp.Name, titleID, excludeAnyApp.AdamID, team.ID, excludeAnyApp.Platform, l2.ID, l2.Name), 0)

		s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d", titleID), nil, http.StatusOK, &getSWTitle, "team_id", fmt.Sprint(team.ID))
		require.NotNil(t, getSWTitle.SoftwareTitle.AppStoreApp)
		require.Equal(t, getSWTitle.SoftwareTitle.AppStoreApp.AdamID, excludeAnyApp.AdamID)
		require.Empty(t, getSWTitle.SoftwareTitle.AppStoreApp.LabelsIncludeAny)
		require.Equal(t, getSWTitle.SoftwareTitle.AppStoreApp.LabelsExcludeAny, []fleet.SoftwareScopeLabel{{LabelName: l2.Name, LabelID: l2.ID}})
		require.True(t, getSWTitle.SoftwareTitle.AppStoreApp.SelfService)

		// Add a non-VPP software
		payloadRubyTm1 := &fleet.UploadSoftwareInstallerPayload{
			InstallScript: "install",
			Filename:      "ruby.deb",
			SelfService:   false,
			TeamID:        &team.ID,
		}
		s.uploadSoftwareInstaller(t, payloadRubyTm1, http.StatusOK, "")

		resp := listSoftwareTitlesResponse{}
		s.DoJSON(
			"GET", "/api/latest/fleet/software/titles",
			listSoftwareTitlesRequest{},
			http.StatusOK, &resp,
			"query", "ruby",
			"team_id", fmt.Sprintf("%d", team.ID),
		)

		require.Len(t, resp.SoftwareTitles, 1)
		nonVPPTitleID := resp.SoftwareTitles[0].ID

		updateAppReq := &updateAppStoreAppRequest{TeamID: &team.ID, SelfService: ptr.Bool(false)}

		// Attempt to update the non-VPP software using the VPP path. Should fail.
		s.Do("PATCH", fmt.Sprintf("/api/latest/fleet/software/titles/%d/app_store_app", nonVPPTitleID), updateAppReq, http.StatusNotFound)

		// Attempt to update a non-existent app. Should fail.
		s.Do("PATCH", "/api/latest/fleet/software/titles/9999/app_store_app", updateAppReq, http.StatusNotFound)

		// Attempt to update with both types of labels. Should fail.
		updateAppReq.LabelsIncludeAny = []string{l1.Name}
		updateAppReq.LabelsExcludeAny = []string{l1.Name}
		res = s.Do("PATCH", fmt.Sprintf("/api/latest/fleet/software/titles/%d/app_store_app", titleID), updateAppReq, http.StatusBadRequest)
		require.Contains(t, extractServerErrorText(res.Body), `Only one of "labels_include_any" or "labels_exclude_any" can be included.`)

		// Attempt to update with a non-existent label. Should fail.
		updateAppReq.LabelsExcludeAny = []string{}
		updateAppReq.LabelsIncludeAny = []string{"404_notfound"}
		res = s.Do("PATCH", fmt.Sprintf("/api/latest/fleet/software/titles/%d/app_store_app", titleID), updateAppReq, http.StatusBadRequest)
		require.Contains(t, extractServerErrorText(res.Body), "some or all the labels provided don't exist")

		// Update App2. Unset self service and update the labels
		updateAppReq.LabelsIncludeAny = []string{l2.Name}
		var updateAppResp updateAppStoreAppResponse
		s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/software/titles/%d/app_store_app", titleID), updateAppReq, http.StatusOK, &updateAppResp)

		require.NotNil(t, updateAppResp.AppStoreApp)
		require.Equal(t, updateAppResp.AppStoreApp.AdamID, excludeAnyApp.AdamID)
		require.Equal(t, updateAppResp.AppStoreApp.LabelsIncludeAny, []fleet.SoftwareScopeLabel{{LabelName: l2.Name, LabelID: l2.ID}})
		require.Empty(t, updateAppResp.AppStoreApp.LabelsExcludeAny)
		require.False(t, updateAppResp.AppStoreApp.SelfService)
		require.Equal(t, fleet.MacOSPlatform, updateAppResp.AppStoreApp.Platform)

		activityData = `{"team_name": "%s", "software_title": "%s", "app_store_id": "%s", "software_icon_url": "https://example.com/images/2", "team_id": %d, "software_title_id": %d, "platform": "%s", "self_service": false, "labels_include_any": [{"id": %d, "name": %q}], "software_display_name": ""}`
		s.lastActivityMatches(fleet.ActivityEditedAppStoreApp{}.ActivityName(),
			fmt.Sprintf(activityData, team.Name,
				excludeAnyApp.Name, excludeAnyApp.AdamID, team.ID, titleID, excludeAnyApp.Platform, l2.ID, l2.Name), 0)

		// double check that our updates worked
		getSWTitle = getSoftwareTitleResponse{}
		s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d", titleID), nil, http.StatusOK, &getSWTitle, "team_id", fmt.Sprint(team.ID))
		require.NotNil(t, getSWTitle.SoftwareTitle.AppStoreApp)
		require.Equal(t, getSWTitle.SoftwareTitle.AppStoreApp.AdamID, excludeAnyApp.AdamID)
		require.Equal(t, getSWTitle.SoftwareTitle.AppStoreApp.LabelsIncludeAny, []fleet.SoftwareScopeLabel{{LabelName: l2.Name, LabelID: l2.ID}})
		require.Empty(t, getSWTitle.SoftwareTitle.AppStoreApp.LabelsExcludeAny)
		require.False(t, getSWTitle.SoftwareTitle.AppStoreApp.SelfService)

		// Attempt an install on the host. This should fail because the host doesn't have the label
		// l2.
		res = s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/install", host.ID, titleID), &installSoftwareRequest{}, http.StatusBadRequest)
		require.Contains(t, extractServerErrorText(res.Body), "Couldn't install. This host isn't a member of the labels defined for this software title.")

		// Add l2 to the host. Attempt install again, should succeed
		s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/labels/%d", l2.ID), &fleet.ModifyLabelPayload{Hosts: []string{host.HardwareSerial}}, http.StatusOK, &createLabelResp)

		s.appleVPPConfigSrvConfig.SerialNumbers = append(s.appleVPPConfigSrvConfig.SerialNumbers, mdmDevice.SerialNumber)

		s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/install", host.ID, titleID), &installSoftwareRequest{}, http.StatusAccepted)

		// Attempt to delete a non-existent app. Should fail.
		s.Do("DELETE", "/api/latest/fleet/software/titles/9999/available_for_install", nil, http.StatusNotFound, "team_id", fmt.Sprintf("%d", team.ID))

		// delete the VPP app
		s.Do("DELETE", fmt.Sprintf("/api/latest/fleet/software/titles/%d/available_for_install", titleID), nil, http.StatusNoContent, "team_id", fmt.Sprintf("%d", team.ID))
		activityData = `{"team_name": "%s", "software_title": "%s", "app_store_id": "%s", "software_icon_url": "https://example.com/images/2", "team_id": %d, "platform": "%s", "labels_include_any": [{"id": %d, "name": %q}]}`
		s.lastActivityMatches(fleet.ActivityDeletedAppStoreApp{}.ActivityName(),
			fmt.Sprintf(activityData, team.Name,
				excludeAnyApp.Name, excludeAnyApp.AdamID, team.ID, excludeAnyApp.Platform, l2.ID, l2.Name), 0)

		// iOS and iPadOS

		iOSApp := fleet.VPPApp{
			VPPAppTeam: fleet.VPPAppTeam{
				VPPAppID: fleet.VPPAppID{
					AdamID:   "2",
					Platform: fleet.IOSPlatform,
				},
			},
			Name:             "App 2",
			BundleIdentifier: "b-2",
			IconURL:          "https://example.com/images/2",
			LatestVersion:    "2.0.0",
		}

		iPadOSApp := fleet.VPPApp{
			VPPAppTeam: fleet.VPPAppTeam{
				VPPAppID: fleet.VPPAppID{
					AdamID:   "2",
					Platform: fleet.IPadOSPlatform,
				},
			},
			Name:             "App 2",
			BundleIdentifier: "b-2",
			IconURL:          "https://example.com/images/2",
			LatestVersion:    "2.0.0",
		}

		testCases := []struct {
			desc      string
			app       fleet.VPPApp
			addAppReq *addAppStoreAppRequest
		}{
			{
				desc: "ios device",
				app:  iOSApp,
				addAppReq: &addAppStoreAppRequest{
					TeamID:           &team.ID,
					AppStoreID:       iOSApp.AdamID,
					LabelsIncludeAny: []string{l2.Name},
					Platform:         iOSApp.Platform,
				},
			},

			{
				desc: "ipados device",
				app:  iPadOSApp,
				addAppReq: &addAppStoreAppRequest{
					TeamID:           &team.ID,
					AppStoreID:       iPadOSApp.AdamID,
					LabelsIncludeAny: []string{l2.Name},
					Platform:         iPadOSApp.Platform,
				},
			},
		}
		for _, tc := range testCases {
			t.Run(tc.desc, func(t *testing.T) {
				mobileHost, mobileMDMDevice := s.createAppleMobileHostThenEnrollMDM(string(tc.app.Platform))
				s.Do("POST", "/api/latest/fleet/hosts/transfer", &addHostsToTeamRequest{HostIDs: []uint{mobileHost.ID}, TeamID: &team.ID}, http.StatusOK)

				s.Do("POST", "/api/latest/fleet/software/app_store_apps", tc.addAppReq, http.StatusOK)
				titleID = getSoftwareTitleIDFromApp(&tc.app)
				s.appleVPPConfigSrvConfig.SerialNumbers = append(s.appleVPPConfigSrvConfig.SerialNumbers, mobileMDMDevice.SerialNumber)

				// Should fail because label is include_any and the host doesn't have it
				res = s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/install", mobileHost.ID, titleID), &installSoftwareRequest{}, http.StatusBadRequest)
				require.Contains(t, extractServerErrorText(res.Body), "Couldn't install. This host isn't a member of the labels defined for this software title.")

				// Create a new label
				s.DoJSON("POST", "/api/latest/fleet/labels", &fleet.LabelPayload{Name: "label3" + t.Name(), Hosts: []string{mobileMDMDevice.SerialNumber}}, http.StatusOK, &createLabelResp)
				l3 := createLabelResp.Label
				require.NotNil(t, l3)

				// Should fail because label is exclude_any and the host does have it
				updateAppReq = &updateAppStoreAppRequest{TeamID: &team.ID, LabelsExcludeAny: []string{l3.Name}}
				s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/software/titles/%d/app_store_app", titleID), updateAppReq, http.StatusOK, &updateAppResp)
				res = s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/install", mobileHost.ID, titleID), &installSoftwareRequest{}, http.StatusBadRequest)
				require.Contains(t, extractServerErrorText(res.Body), "Couldn't install. This host isn't a member of the labels defined for this software title.")

				// Should succeed because the label is include_any and the host does have it
				updateAppReq = &updateAppStoreAppRequest{TeamID: &team.ID, LabelsIncludeAny: []string{l3.Name}}
				s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/software/titles/%d/app_store_app", titleID), updateAppReq, http.StatusOK, &updateAppResp)
				s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/install", mobileHost.ID, titleID), &installSoftwareRequest{}, http.StatusAccepted)

				// Create a new host
				mobileHost, mobileMDMDevice = s.createAppleMobileHostThenEnrollMDM(string(tc.app.Platform))
				s.Do("POST", "/api/latest/fleet/hosts/transfer", &addHostsToTeamRequest{HostIDs: []uint{mobileHost.ID}, TeamID: &team.ID}, http.StatusOK)
				s.appleVPPConfigSrvConfig.SerialNumbers = append(s.appleVPPConfigSrvConfig.SerialNumbers, mobileMDMDevice.SerialNumber)

				// Should succeed because the label is exclude_any and the iOS host doesn't have it
				updateAppReq = &updateAppStoreAppRequest{TeamID: &team.ID, LabelsExcludeAny: []string{l3.Name}}
				s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/software/titles/%d/app_store_app", titleID), updateAppReq, http.StatusOK, &updateAppResp)
				s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/install", mobileHost.ID, titleID), &installSoftwareRequest{}, http.StatusAccepted)
			})
		}
	})

	// Create a team
	var newTeamResp teamResponse
	s.DoJSON("POST", "/api/latest/fleet/teams", &createTeamRequest{TeamPayload: fleet.TeamPayload{Name: ptr.String("Team 1")}}, http.StatusOK, &newTeamResp)
	team := newTeamResp.Team

	// Associate team to the VPP token.
	var resPatchVPP patchVPPTokensTeamsResponse
	s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/vpp_tokens/%d/teams", resp.Tokens[0].ID), patchVPPTokensTeamsRequest{TeamIDs: []uint{team.ID}}, http.StatusOK, &resPatchVPP)

	// A PATCH endpoint omitting mdm.volume_purchasing_program should not remove the VPP token association.
	acResp := appConfigResponse{}
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
		"agent_options": {
			"config": {
				"options": {
					"pack_delimiter": "/",
					"logger_tls_period": 10,
					"distributed_plugin": "tls",
					"disable_distributed": false,
					"logger_tls_endpoint": "/api/osquery/log",
					"distributed_interval": 10,
					"distributed_tls_max_attempts": 3
				}
			}
		}
  }`), http.StatusOK, &acResp)

	// Check that the VPP token is still associated to the team.
	resp = getVPPTokensResponse{}
	s.DoJSON("GET", "/api/latest/fleet/vpp_tokens", &getVPPTokensRequest{}, http.StatusOK, &resp)
	require.NoError(t, resp.Err)
	require.Len(t, resp.Tokens, 1)
	require.Equal(t, orgName, resp.Tokens[0].OrgName)
	require.Equal(t, location, resp.Tokens[0].Location)
	require.Equal(t, expTime, resp.Tokens[0].RenewDate)
	require.Len(t, resp.Tokens[0].Teams, 1)
	require.Equal(t, team.ID, resp.Tokens[0].Teams[0].ID)
	require.Equal(t, team.Name, resp.Tokens[0].Teams[0].Name)

	// Reset the token's teams by omitting the token from app config
	acResp = appConfigResponse{}
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
		"mdm": { "volume_purchasing_program": [] }
  }`), http.StatusOK, &acResp)

	resp = getVPPTokensResponse{}
	s.DoJSON("GET", "/api/latest/fleet/vpp_tokens", &getVPPTokensRequest{}, http.StatusOK, &resp)
	require.NoError(t, resp.Err)
	require.Len(t, resp.Tokens, 1)
	require.Equal(t, orgName, resp.Tokens[0].OrgName)
	require.Equal(t, location, resp.Tokens[0].Location)
	require.Equal(t, expTime, resp.Tokens[0].RenewDate)
	require.Empty(t, resp.Tokens[0].Teams)

	// Add the team back using the PATCH /api/latest/fleet/config endpoint now (what GitOps uses).
	acResp = appConfigResponse{}
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(fmt.Sprintf(`{
		"mdm": { "volume_purchasing_program": [ {"location": "%s", "teams": [ "%s" ]} ] }
  }`, location, team.Name)), http.StatusOK, &acResp)

	// Check again that the VPP token is associated to the team.
	resp = getVPPTokensResponse{}
	s.DoJSON("GET", "/api/latest/fleet/vpp_tokens", &getVPPTokensRequest{}, http.StatusOK, &resp)
	require.NoError(t, resp.Err)
	require.Len(t, resp.Tokens, 1)
	require.Equal(t, orgName, resp.Tokens[0].OrgName)
	require.Equal(t, location, resp.Tokens[0].Location)
	require.Equal(t, expTime, resp.Tokens[0].RenewDate)
	require.Len(t, resp.Tokens[0].Teams, 1)
	require.Equal(t, team.ID, resp.Tokens[0].Teams[0].ID)
	require.Equal(t, team.Name, resp.Tokens[0].Teams[0].Name)

	// Get list of VPP apps from "Apple"
	// We're passing team 1 here, but we haven't added any app store apps to that team, so we get
	// back all available apps in our VPP location.
	var appResp getAppStoreAppsResponse
	s.DoJSON("GET", "/api/latest/fleet/software/app_store_apps", &getAppStoreAppsRequest{}, http.StatusOK, &appResp, "team_id",
		fmt.Sprint(team.ID))
	require.NoError(t, appResp.Err)

	macOSApp := fleet.VPPApp{
		VPPAppTeam: fleet.VPPAppTeam{
			VPPAppID: fleet.VPPAppID{
				AdamID:   "1",
				Platform: fleet.MacOSPlatform,
			},
		},
		Name:             "App 1",
		BundleIdentifier: "a-1",
		IconURL:          "https://example.com/images/1",
		LatestVersion:    "1.0.0",
	}
	iPadOSApp := fleet.VPPApp{
		VPPAppTeam: fleet.VPPAppTeam{
			VPPAppID: fleet.VPPAppID{
				AdamID:   "2",
				Platform: fleet.IPadOSPlatform,
			},
		},
		Name:             "App 2",
		BundleIdentifier: "b-2",
		IconURL:          "https://example.com/images/2",
		LatestVersion:    "2.0.0",
	}
	iOSApp := fleet.VPPApp{
		VPPAppTeam: fleet.VPPAppTeam{
			VPPAppID: fleet.VPPAppID{
				AdamID:   "2",
				Platform: fleet.IOSPlatform,
			},
		},
		Name:             "App 2",
		BundleIdentifier: "b-2",
		IconURL:          "https://example.com/images/2",
		LatestVersion:    "2.0.0",
	}
	expectedApps := []*fleet.VPPApp{
		&macOSApp,
		&iPadOSApp,
		&iOSApp,
		{
			VPPAppTeam: fleet.VPPAppTeam{
				VPPAppID: fleet.VPPAppID{
					AdamID:   "2",
					Platform: fleet.MacOSPlatform,
				},
			},
			Name:             "App 2",
			BundleIdentifier: "b-2",
			IconURL:          "https://example.com/images/2",
			LatestVersion:    "2.0.0",
		},
		{
			VPPAppTeam: fleet.VPPAppTeam{
				VPPAppID: fleet.VPPAppID{
					AdamID:   "3",
					Platform: fleet.IPadOSPlatform,
				},
			},
			Name:             "App 3",
			BundleIdentifier: "c-3",
			IconURL:          "https://example.com/images/3",
			LatestVersion:    "3.0.0",
		},
	}
	assert.ElementsMatch(t, expectedApps, appResp.AppStoreApps)

	// Insert/deletion flow for macOS app
	// Add an app store app to team 1
	addedApp := expectedApps[0]
	var addAppResp addAppStoreAppResponse
	// Add an app store app to non-existent team
	s.DoJSON("POST", "/api/latest/fleet/software/app_store_apps", &addAppStoreAppRequest{TeamID: ptr.Uint(9999), AppStoreID: addedApp.AdamID}, http.StatusNotFound, &addAppResp)

	s.DoJSON("POST", "/api/latest/fleet/software/app_store_apps", &addAppStoreAppRequest{TeamID: &team.ID, AppStoreID: addedApp.AdamID, SelfService: true, AutomaticInstall: true}, http.StatusOK, &addAppResp)

	s.lastActivityOfTypeMatches(fleet.ActivityAddedAppStoreApp{}.ActivityName(),
		fmt.Sprintf(`{"team_name": "%s", "software_title": "%s", "software_title_id": %d, "app_store_id": "%s", "team_id": %d, "platform": "%s", "self_service": true}`, team.Name,
			addedApp.Name, getSoftwareTitleIDFromApp(addedApp), addedApp.AdamID, team.ID, addedApp.Platform), 0)

	// Now we should be filtering out the app we added to team 1
	appResp = getAppStoreAppsResponse{}
	s.DoJSON("GET", "/api/latest/fleet/software/app_store_apps", &getAppStoreAppsRequest{}, http.StatusOK, &appResp, "team_id",
		fmt.Sprint(team.ID))
	require.NoError(t, appResp.Err)
	assert.ElementsMatch(t, expectedApps[1:], appResp.AppStoreApps)

	// list the software titles for that team, to get the title id of the VPP app
	var listSw listSoftwareTitlesResponse
	s.DoJSON("GET", "/api/latest/fleet/software/titles", nil, http.StatusOK, &listSw, "team_id", fmt.Sprint(team.ID), "available_for_install", "true")
	require.Len(t, listSw.SoftwareTitles, 1)
	require.True(t, *listSw.SoftwareTitles[0].AppStoreApp.SelfService)
	macOSTitleID := listSw.SoftwareTitles[0].ID
	require.Len(t, listSw.SoftwareTitles[0].AppStoreApp.AutomaticInstallPolicies, 1)

	// check that we created an activity for the policy creation
	wantAct := fleet.ActivityTypeCreatedPolicy{
		ID:   listSw.SoftwareTitles[0].AppStoreApp.AutomaticInstallPolicies[0].ID,
		Name: listSw.SoftwareTitles[0].AppStoreApp.AutomaticInstallPolicies[0].Name,
	}
	s.lastActivityMatches(wantAct.ActivityName(), string(jsonMustMarshal(t, wantAct)), 0)

	// listing with the self-service filter also returns it
	listSw = listSoftwareTitlesResponse{}
	s.DoJSON("GET", "/api/latest/fleet/software/titles", nil, http.StatusOK, &listSw, "team_id", fmt.Sprint(team.ID), "self_service", "true")
	require.Len(t, listSw.SoftwareTitles, 1)
	require.Equal(t, macOSTitleID, listSw.SoftwareTitles[0].ID)

	// delete the automatic install policy (so we can delete the app next)
	var deletePolicyResp deleteTeamPoliciesResponse
	s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/teams/%d/policies/delete", team.ID), &deleteGlobalPoliciesRequest{IDs: []uint{listSw.SoftwareTitles[0].AppStoreApp.AutomaticInstallPolicies[0].ID}}, http.StatusOK, &deletePolicyResp)

	// delete the app store app for team 1
	s.Do("DELETE", fmt.Sprintf("/api/latest/fleet/software/titles/%d/available_for_install", macOSTitleID), nil, http.StatusNoContent,
		"team_id", fmt.Sprint(team.ID))
	s.lastActivityMatches(fleet.ActivityDeletedAppStoreApp{}.ActivityName(),
		fmt.Sprintf(`{"team_name": "%s", "software_title": "%s", "app_store_id": "%s", "software_icon_url": "https://example.com/images/1", "team_id": %d, "platform": "%s"}`, team.Name,
			addedApp.Name, addedApp.AdamID, team.ID, addedApp.Platform), 0)

	// deleting it again fails, not found
	s.Do("DELETE", fmt.Sprintf("/api/latest/fleet/software/titles/%d/available_for_install", macOSTitleID), nil, http.StatusNotFound,
		"team_id", fmt.Sprint(team.ID))

	// get the list of available apps, returns all apps now
	appResp = getAppStoreAppsResponse{}
	s.DoJSON("GET", "/api/latest/fleet/software/app_store_apps", nil, http.StatusOK, &appResp, "team_id", fmt.Sprint(team.ID))
	require.NoError(t, appResp.Err)
	assert.ElementsMatch(t, expectedApps, appResp.AppStoreApps)

	// Insert/deletion flow for iPadOS app
	addedApp = expectedApps[1]
	addAppResp = addAppStoreAppResponse{}
	// Accepts self-service for iPadOS
	s.DoJSON("POST", "/api/latest/fleet/software/app_store_apps",
		&addAppStoreAppRequest{TeamID: &team.ID, AppStoreID: addedApp.AdamID, Platform: addedApp.Platform, SelfService: true},
		http.StatusOK, &addAppResp)
	// No auto-install for iPadOS
	s.DoJSON("POST", "/api/latest/fleet/software/app_store_apps",
		&addAppStoreAppRequest{TeamID: &team.ID, AppStoreID: addedApp.AdamID, Platform: addedApp.Platform, AutomaticInstall: true},
		http.StatusBadRequest, &addAppResp)
	s.DoJSON("POST", "/api/latest/fleet/software/app_store_apps",
		&addAppStoreAppRequest{TeamID: &team.ID, AppStoreID: addedApp.AdamID, Platform: addedApp.Platform},
		http.StatusOK, &addAppResp)
	s.lastActivityMatches(fleet.ActivityAddedAppStoreApp{}.ActivityName(),
		fmt.Sprintf(`{"team_name": "%s", "software_title": "%s", "software_title_id": %d, "app_store_id": "%s", "team_id": %d, "platform": "%s", "self_service": false}`, team.Name,
			addedApp.Name, getSoftwareTitleIDFromApp(addedApp), addedApp.AdamID, team.ID, addedApp.Platform), 0)

	// Now we should be filtering out the app we added to team 1
	appResp = getAppStoreAppsResponse{}
	s.DoJSON("GET", "/api/latest/fleet/software/app_store_apps", &getAppStoreAppsRequest{}, http.StatusOK, &appResp, "team_id",
		fmt.Sprint(team.ID))
	require.NoError(t, appResp.Err)
	assert.ElementsMatch(t, append(expectedApps[2:], expectedApps[0]), appResp.AppStoreApps)

	// list the software titles for that team, to get the title id of the VPP app
	listSw = listSoftwareTitlesResponse{}
	s.DoJSON("GET", "/api/latest/fleet/software/titles", nil, http.StatusOK, &listSw, "team_id", fmt.Sprint(team.ID),
		"available_for_install", "true")
	require.Len(t, listSw.SoftwareTitles, 1)
	macOSTitleID = listSw.SoftwareTitles[0].ID
	assert.Equal(t, "ipados_apps", listSw.SoftwareTitles[0].Source)

	// filtering by self-service returns nothing
	listSw = listSoftwareTitlesResponse{}
	s.DoJSON("GET", "/api/latest/fleet/software/titles", nil, http.StatusOK, &listSw, "team_id", fmt.Sprint(team.ID),
		"self_service", "true")
	require.Len(t, listSw.SoftwareTitles, 0)

	_, err := s.ds.CreateOrUpdateSoftwareTitleIcon(context.Background(), &fleet.UploadSoftwareTitleIconPayload{
		TitleID:   macOSTitleID,
		Filename:  "icon.png",
		TeamID:    team.ID,
		StorageID: "icon_storage_id",
	})
	require.NoError(t, err)

	// delete the app store app for team 1
	s.Do("DELETE", fmt.Sprintf("/api/latest/fleet/software/titles/%d/available_for_install", macOSTitleID), nil, http.StatusNoContent,
		"team_id",
		fmt.Sprint(team.ID))
	s.lastActivityMatches(fleet.ActivityDeletedAppStoreApp{}.ActivityName(),
		fmt.Sprintf(`{"team_name": "%s", "software_title": "%s", "app_store_id": "%s", "software_icon_url": "/api/latest/fleet/software/titles/%d/icon?team_id=%d", "team_id": %d, "platform": "%s"}`, team.Name,
			addedApp.Name, addedApp.AdamID, macOSTitleID, team.ID, team.ID, addedApp.Platform), 0)

	var count int
	mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
		return sqlx.GetContext(context.Background(), q, &count, `SELECT COUNT(1) FROM software_title_icons WHERE team_id = ? and software_title_id = ?`, team.ID, macOSTitleID)
	})
	require.Equal(t, 0, count)

	// deleting it again fails, not found
	s.Do("DELETE", fmt.Sprintf("/api/latest/fleet/software/titles/%d/available_for_install", macOSTitleID), nil, http.StatusNotFound,
		"team_id",
		fmt.Sprint(team.ID))

	// get the list of available apps, returns all apps now
	appResp = getAppStoreAppsResponse{}
	s.DoJSON("GET", "/api/latest/fleet/software/app_store_apps", nil, http.StatusOK, &appResp, "team_id", fmt.Sprint(team.ID))
	require.NoError(t, appResp.Err)
	assert.ElementsMatch(t, expectedApps, appResp.AppStoreApps)

	// Installation flow

	// Create a couple of hosts
	orbitHost := createOrbitEnrolledHost(t, "darwin", "nonmdm", s.ds)
	mdmHost, mdmDevice := createHostThenEnrollMDM(s.ds, s.server.URL, t)
	setOrbitEnrollment(t, mdmHost, s.ds)
	s.runWorker()
	checkInstallFleetdCommandSent(t, mdmDevice, true)
	selfServiceHost, selfServiceDevice := createHostThenEnrollMDM(s.ds, s.server.URL, t)
	setOrbitEnrollment(t, selfServiceHost, s.ds)
	s.runWorker()
	checkInstallFleetdCommandSent(t, selfServiceDevice, true)
	selfServiceToken := "selfservicetoken"
	updateDeviceTokenForHost(t, s.ds, selfServiceHost.ID, selfServiceToken)
	s.appleVPPConfigSrvConfig.SerialNumbers = append(s.appleVPPConfigSrvConfig.SerialNumbers, selfServiceDevice.SerialNumber)
	iOSHost, iOSMdmClient := s.createAppleMobileHostThenEnrollMDM("ios")
	iPadOSHost, iPadOSMdmClient := s.createAppleMobileHostThenEnrollMDM("ipados")
	// ensure a valid alternate device token for self-service status access checking later
	updateDeviceTokenForHost(t, s.ds, mdmHost.ID, "foobar")

	// Add serial number to our fake Apple server
	s.appleVPPConfigSrvConfig.SerialNumbers = append(s.appleVPPConfigSrvConfig.SerialNumbers, mdmHost.HardwareSerial,
		iOSHost.HardwareSerial, iPadOSHost.HardwareSerial)
	s.Do("POST", "/api/latest/fleet/hosts/transfer",
		&addHostsToTeamRequest{HostIDs: []uint{mdmHost.ID, orbitHost.ID, iOSHost.ID, iPadOSHost.ID, selfServiceHost.ID}, TeamID: &team.ID}, http.StatusOK)

	// Add all apps to the team
	addedApp = expectedApps[0]
	errApp := expectedApps[3]
	appSelfService := expectedApps[0]
	// Add app 1 as self-service
	addAppResp = addAppStoreAppResponse{}
	s.DoJSON("POST", "/api/latest/fleet/software/app_store_apps",
		&addAppStoreAppRequest{TeamID: &team.ID, AppStoreID: appSelfService.AdamID, Platform: appSelfService.Platform, SelfService: true},
		http.StatusOK, &addAppResp)
	s.lastActivityMatches(
		fleet.ActivityAddedAppStoreApp{}.ActivityName(),
		fmt.Sprintf(`{"team_name": "%s", "software_title": "%s", "software_title_id": %d, "app_store_id": "%s", "team_id": %d, "platform": "%s", "self_service": true}`, team.Name,
			appSelfService.Name, getSoftwareTitleIDFromApp(appSelfService), appSelfService.AdamID, team.ID, appSelfService.Platform),
		0,
	)
	listSw = listSoftwareTitlesResponse{}
	s.DoJSON("GET", "/api/latest/fleet/software/titles", nil, http.StatusOK, &listSw, "team_id", fmt.Sprint(team.ID),
		"available_for_install", "true")
	// Add remaining as non-self-service
	for _, app := range expectedApps[1:] {
		addAppResp = addAppStoreAppResponse{}
		s.DoJSON("POST", "/api/latest/fleet/software/app_store_apps",
			&addAppStoreAppRequest{TeamID: &team.ID, AppStoreID: app.AdamID, Platform: app.Platform},
			http.StatusOK, &addAppResp)
		s.lastActivityMatches(
			fleet.ActivityAddedAppStoreApp{}.ActivityName(),
			fmt.Sprintf(`{"team_name": "%s", "software_title": "%s", "software_title_id": %d, "app_store_id": "%s", "team_id": %d, "platform": "%s", "self_service": false}`, team.Name,
				app.Name, getSoftwareTitleIDFromApp(app), app.AdamID, team.ID, app.Platform),
			0,
		)
		listSw = listSoftwareTitlesResponse{}
		s.DoJSON("GET", "/api/latest/fleet/software/titles", nil, http.StatusOK, &listSw, "team_id", fmt.Sprint(team.ID),
			"available_for_install", "true")
	}

	listSw = listSoftwareTitlesResponse{}
	s.DoJSON("GET", "/api/latest/fleet/software/titles", nil, http.StatusOK, &listSw, "team_id", fmt.Sprint(team.ID), "available_for_install", "true")
	require.Len(t, listSw.SoftwareTitles, len(expectedApps))
	macOSTitleID = 99999
	errTitleID := uint(99999)
	iOSTitleID := uint(99999)
	iPadOSTitleID := uint(99999)
	for _, sw := range listSw.SoftwareTitles {
		assert.NotNil(t, sw.AppStoreApp)
		switch {
		case sw.Name == addedApp.Name && sw.Source == "apps":
			macOSTitleID = sw.ID
		case sw.Name == errApp.Name && sw.Source == "apps":
			errTitleID = sw.ID
		case sw.Name == iOSApp.Name && sw.Source == "ios_apps":
			iOSTitleID = sw.ID
		case sw.Name == iPadOSApp.Name && sw.Source == "ipados_apps":
			iPadOSTitleID = sw.ID
		}
	}

	// attempt to install a VPP app on the non-MDM enrolled host
	r := s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/install", orbitHost.ID, macOSTitleID), &installSoftwareRequest{}, http.StatusBadRequest)
	require.Contains(t, extractServerErrorText(r.Body), "Error: Couldn't install. To install App Store app, turn on MDM for this host.")

	// Disable all teams token
	s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/vpp_tokens/%d/teams", validToken.Token.ID), patchVPPTokensTeamsRequest{}, http.StatusOK, &resPatchVPP)

	// Spoof an expired VPP token and attempt to install VPP app
	tokenJSONBad := fmt.Sprintf(`{"expDate":"%s","token":"%s","orgName":"%s"}`, "2030-06-24T15:50:50+0000", "badtoken", "Evil Fleet")
	s.appleVPPConfigSrvConfig.Location = "Spooky Haunted House"
	var vppRes uploadVPPTokenResponse
	s.uploadDataViaForm("/api/latest/fleet/vpp_tokens", "token", "token.vpptoken", []byte(base64.StdEncoding.EncodeToString([]byte(tokenJSONBad))), http.StatusAccepted, "", &vppRes)

	s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/vpp_tokens/%d/teams", vppRes.Token.ID), patchVPPTokensTeamsRequest{TeamIDs: []uint{team.ID, 99}}, http.StatusUnprocessableEntity, &resPatchVPP)

	s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/vpp_tokens/%d/teams", vppRes.Token.ID), patchVPPTokensTeamsRequest{TeamIDs: []uint{team.ID}}, http.StatusOK, &resPatchVPP)

	// Disable the token
	s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/vpp_tokens/%d/teams", vppRes.Token.ID), patchVPPTokensTeamsRequest{}, http.StatusOK, &resPatchVPP)

	// Enable all teams token
	s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/vpp_tokens/%d/teams", validToken.Token.ID), patchVPPTokensTeamsRequest{TeamIDs: []uint{}}, http.StatusOK, &resPatchVPP)

	// Attempt to install non-existent app
	r = s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/install", mdmHost.ID, 99999), &installSoftwareRequest{},
		http.StatusBadRequest)
	require.Contains(t, extractServerErrorText(r.Body), "Couldn't install software. Software title is not available for install. Please add software package or App Store app to install.")

	// Add app 1 as self-service
	addAppResp = addAppStoreAppResponse{}
	s.DoJSON("POST", "/api/latest/fleet/software/app_store_apps",
		&addAppStoreAppRequest{TeamID: &team.ID, AppStoreID: errApp.AdamID, Platform: errApp.Platform, SelfService: true},
		http.StatusOK, &addAppResp)

	// Add remaining apps without self-service
	for _, app := range expectedApps {
		addAppResp = addAppStoreAppResponse{}
		s.DoJSON("POST", "/api/latest/fleet/software/app_store_apps",
			&addAppStoreAppRequest{TeamID: &team.ID, AppStoreID: app.AdamID, Platform: app.Platform, SelfService: app.AdamID == macOSApp.AdamID},
			http.StatusOK, &addAppResp)
	}

	// Trigger install to the host
	installResp := installSoftwareResponse{}
	s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/install", mdmHost.ID, errTitleID), &installSoftwareRequest{},
		http.StatusAccepted, &installResp)

	s.Do("DELETE", fmt.Sprintf("/api/latest/fleet/vpp_tokens/%d", vppRes.Token.ID), &deleteVPPTokenRequest{}, http.StatusNoContent)

	// Check if the host is listed as pending
	var listResp listHostsResponse
	s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listResp, "software_status", "pending", "team_id", fmt.Sprint(team.ID),
		"software_title_id", fmt.Sprint(errTitleID))
	require.Len(t, listResp.Hosts, 1)
	require.Equal(t, listResp.Hosts[0].ID, mdmHost.ID)
	var countResp countHostsResponse
	s.DoJSON("GET", "/api/latest/fleet/hosts/count", nil, http.StatusOK, &countResp, "software_status", "pending", "team_id",
		fmt.Sprint(team.ID), "software_title_id", fmt.Sprint(errTitleID))
	require.Equal(t, 1, countResp.Count)

	// Simulate failed installation on the host
	cmd, err := mdmDevice.Idle()
	var failedCmdUUID string
	require.NoError(t, err)
	assert.NotNil(t, cmd)
	for cmd != nil {
		var fullCmd micromdm.CommandPayload
		switch cmd.Command.RequestType { //nolint:gocritic // ignore singleCaseSwitch
		case "InstallApplication":
			require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd))
			failedCmdUUID = cmd.CommandUUID
			t.Logf("Failed command UUID: %s", failedCmdUUID)
			cmd, err = mdmDevice.Err(cmd.CommandUUID, []mdm.ErrorChain{{ErrorCode: 1234}})
			require.NoError(t, err)
		}
	}

	listResp = listHostsResponse{}
	s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listResp, "software_status", "failed", "team_id", fmt.Sprint(team.ID),
		"software_title_id", fmt.Sprint(errTitleID))
	require.Len(t, listResp.Hosts, 1)
	require.Equal(t, listResp.Hosts[0].ID, mdmHost.ID)
	countResp = countHostsResponse{}
	s.DoJSON("GET", "/api/latest/fleet/hosts/count", nil, http.StatusOK, &countResp, "software_status", "failed", "team_id",
		fmt.Sprint(team.ID), "software_title_id", fmt.Sprint(errTitleID))
	require.Equal(t, 1, countResp.Count)

	s.lastActivityMatches(
		fleet.ActivityInstalledAppStoreApp{}.ActivityName(),
		fmt.Sprintf(
			`{"host_id": %d, "host_display_name": "%s", "software_title": "%s", "app_store_id": "%s", "command_uuid": "%s", "status": "%s", "self_service": false, "policy_id": null, "policy_name": null}`,
			mdmHost.ID,
			mdmHost.DisplayName(),
			errApp.Name,
			errApp.AdamID,
			failedCmdUUID,
			fleet.SoftwareInstallFailed,
		),
		0,
	)

	// Trigger install to the host
	installResp = installSoftwareResponse{}
	s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/install", mdmHost.ID, macOSTitleID), &installSoftwareRequest{},
		http.StatusAccepted, &installResp)
	countResp = countHostsResponse{}
	s.DoJSON("GET", "/api/latest/fleet/hosts/count", nil, http.StatusOK, &countResp, "software_status", "pending", "team_id",
		fmt.Sprint(team.ID), "software_title_id", fmt.Sprint(macOSTitleID))
	require.Equal(t, 1, countResp.Count)

	s.runWorker()

	// Simulate successful installation on the host
	var installCmdUUID string
	cmd, err = mdmDevice.Idle()
	require.NoError(t, err)
	for cmd != nil {
		var fullCmd micromdm.CommandPayload
		switch cmd.Command.RequestType { //nolint:gocritic // ignore singleCaseSwitch
		case "InstallApplication":
			require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd))
			installCmdUUID = cmd.CommandUUID
			cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID)
			require.NoError(t, err)
		}
	}

	s.runWorker()

	cmd, err = mdmDevice.Idle()
	require.NoError(t, err)
	for cmd != nil {
		var fullCmd micromdm.CommandPayload
		switch cmd.Command.RequestType { //nolint:gocritic // ignore singleCaseSwitch
		case "InstalledApplicationList":
			require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd))
			cmd, err = mdmDevice.AcknowledgeInstalledApplicationList(mdmDevice.UUID, cmd.CommandUUID, []fleet.Software{{Name: addedApp.Name, BundleIdentifier: addedApp.BundleIdentifier, Version: addedApp.LatestVersion, Installed: true}})
			require.NoError(t, err)
		default:
			require.Fail(t, "unexpected command type", cmd.Command.RequestType)
		}
	}

	listResp = listHostsResponse{}
	s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listResp, "software_status", "installed", "team_id",
		fmt.Sprint(team.ID), "software_title_id", fmt.Sprint(macOSTitleID))
	mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
		var types []string
		err := sqlx.SelectContext(context.Background(), q, &types, "SELECT activity_type FROM upcoming_activities WHERE host_id = ?", mdmHost.ID)
		require.NoError(t, err)
		require.Empty(t, types)
		return nil
	})
	require.Len(t, listResp.Hosts, 1)
	countResp = countHostsResponse{}
	s.DoJSON("GET", "/api/latest/fleet/hosts/count", nil, http.StatusOK, &countResp, "software_status", "installed", "team_id",
		fmt.Sprint(team.ID), "software_title_id", fmt.Sprint(macOSTitleID))
	require.Equal(t, 1, countResp.Count)

	s.lastActivityMatches(
		fleet.ActivityInstalledAppStoreApp{}.ActivityName(),
		fmt.Sprintf(
			`{"host_id": %d, "host_display_name": "%s", "software_title": "%s", "app_store_id": "%s", "command_uuid": "%s", "status": "%s", "self_service": false, "policy_id": null, "policy_name": null}`,
			mdmHost.ID,
			mdmHost.DisplayName(),
			addedApp.Name,
			addedApp.AdamID,
			installCmdUUID,
			fleet.SoftwareInstalled,
		),
		0,
	)

	// Check list host software

	getHostSw := getHostSoftwareResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/software", mdmHost.ID), nil, http.StatusOK, &getHostSw)
	gotSW := getHostSw.Software
	require.Len(t, gotSW, 2) // App 1 and App 2
	got1, got2 := gotSW[0], gotSW[1]
	require.Equal(t, got1.Name, "App 1")
	require.NotNil(t, got1.AppStoreApp)
	require.Equal(t, got1.AppStoreApp.AppStoreID, addedApp.AdamID)
	require.Equal(t, got1.IconUrl, ptr.String(addedApp.IconURL))
	require.Empty(t, got1.AppStoreApp.Name) // Name is only present for installer packages
	require.Equal(t, got1.AppStoreApp.Version, addedApp.LatestVersion)
	require.NotNil(t, got1.Status)
	require.Equal(t, *got1.Status, fleet.SoftwareInstalled)
	require.Equal(t, got1.AppStoreApp.LastInstall.CommandUUID, installCmdUUID)
	require.NotNil(t, got1.AppStoreApp.LastInstall.InstalledAt)
	require.Equal(t, got2.Name, "App 2")
	require.NotNil(t, got2.Status)
	require.Equal(t, *got2.Status, fleet.SoftwareInstallFailed)
	require.NotNil(t, got2.AppStoreApp)
	require.Equal(t, got2.AppStoreApp.AppStoreID, errApp.AdamID)
	require.Equal(t, got2.IconUrl, ptr.String(errApp.IconURL))
	require.Empty(t, got2.AppStoreApp.Name)
	require.Equal(t, got2.AppStoreApp.Version, errApp.LatestVersion)
	require.Equal(t, got2.AppStoreApp.LastInstall.CommandUUID, failedCmdUUID)
	require.NotNil(t, got2.AppStoreApp.LastInstall.InstalledAt)

	// Check with a query
	getHostSw = getHostSoftwareResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/software", mdmHost.ID), nil, http.StatusOK, &getHostSw, "query", "App 1")
	require.Len(t, getHostSw.Software, 1) // App 1 only
	got1 = getHostSw.Software[0]
	require.Equal(t, got1.Name, "App 1")
	require.NotNil(t, got1.AppStoreApp)
	require.NotNil(t, got1.AppStoreApp)
	require.Equal(t, got1.AppStoreApp.AppStoreID, addedApp.AdamID)
	require.Equal(t, got1.IconUrl, ptr.String(addedApp.IconURL))
	require.Empty(t, got1.AppStoreApp.Name)
	require.Equal(t, got1.AppStoreApp.Version, addedApp.LatestVersion)
	require.NotNil(t, got1.Status)
	require.Equal(t, *got1.Status, fleet.SoftwareInstalled)
	require.Equal(t, got1.AppStoreApp.LastInstall.CommandUUID, installCmdUUID)
	require.NotNil(t, got1.AppStoreApp.LastInstall.InstalledAt)

	// Filter the self-service apps for that host
	getHostSw = getHostSoftwareResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/software", mdmHost.ID), nil, http.StatusOK, &getHostSw, "self_service", "true")
	require.Len(t, getHostSw.Software, 1)
	require.Equal(t, appSelfService.Name, got1.Name)

	// Return installed app with software detail query
	distributedReq := submitDistributedQueryResultsRequestShim{
		NodeKey: *mdmHost.NodeKey,
		Results: map[string]json.RawMessage{
			hostDetailQueryPrefix + "software_macos": json.RawMessage(fmt.Sprintf(
				`[{"name": "%s", "version": "%s", "type": "Application (macOS)",
					"bundle_identifier": "%s", "source": "apps", "last_opened_at": "",
					"installed_path": "/Applications/a.app"}]`, addedApp.Name, addedApp.LatestVersion, addedApp.BundleIdentifier)),
		},
		Statuses: map[string]interface{}{
			hostDistributedQueryPrefix + "software_macos": 0,
		},
		Messages: map[string]string{},
		Stats:    map[string]*fleet.Stats{},
	}
	distributedResp := submitDistributedQueryResultsResponse{}
	s.DoJSON("POST", "/api/osquery/distributed/write", distributedReq, http.StatusOK, &distributedResp)

	// Remove the installed app by not returning it
	distributedReq = submitDistributedQueryResultsRequestShim{
		NodeKey: *mdmHost.NodeKey,
		Results: map[string]json.RawMessage{
			hostDetailQueryPrefix + "software_macos": json.RawMessage(`[]`),
		},
		Statuses: map[string]interface{}{
			hostDistributedQueryPrefix + "software_macos": 0,
		},
		Messages: map[string]string{},
		Stats:    map[string]*fleet.Stats{},
	}
	distributedResp = submitDistributedQueryResultsResponse{}
	s.DoJSON("POST", "/api/osquery/distributed/write", distributedReq, http.StatusOK, &distributedResp)

	// Check list host software
	getHostSw = getHostSoftwareResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/software", mdmHost.ID), nil, http.StatusOK, &getHostSw)
	gotSW = getHostSw.Software
	require.Len(t, gotSW, 2) // App 1 and App 2
	got1 = gotSW[0]
	require.Equal(t, got1.Name, "App 1")
	require.NotNil(t, got1.AppStoreApp)
	require.Equal(t, got1.AppStoreApp.AppStoreID, addedApp.AdamID)
	require.Equal(t, got1.IconUrl, ptr.String(addedApp.IconURL))
	require.Empty(t, got1.AppStoreApp.Name) // Name is only present for installer packages
	require.Equal(t, got1.AppStoreApp.Version, addedApp.LatestVersion)
	assert.Nil(t, got1.Status)
	assert.Nil(t, got1.AppStoreApp.LastInstall)

	// Install on iOS and iPadOS devices
	installs := map[string]struct {
		installHost    *fleet.Host
		titleID        uint
		mdmClient      *mdmtest.TestAppleMDMClient
		app            fleet.VPPApp
		extraAvailable int
		hostCount      int
		deviceToken    string
	}{
		"iOS app install": {
			installHost: iOSHost,
			titleID:     iOSTitleID,
			mdmClient:   iOSMdmClient,
			app:         iOSApp,
			hostCount:   1,
		},
		"iPadOS app install": {
			installHost:    iPadOSHost,
			titleID:        iPadOSTitleID,
			mdmClient:      iPadOSMdmClient,
			app:            iPadOSApp,
			extraAvailable: 1,
			hostCount:      1,
		},
		"macOS app install": {
			installHost: selfServiceHost,
			titleID:     macOSTitleID,
			mdmClient:   selfServiceDevice,
			app:         macOSApp,
			hostCount:   2,
			deviceToken: selfServiceToken,
		},
	}

	for name, install := range installs {
		t.Run(name, func(t *testing.T) {
			installHost := install.installHost
			titleID := install.titleID
			mdmClient := install.mdmClient
			app := install.app

			// Self-service install
			if install.deviceToken != "" {
				var ssInstallResp submitSelfServiceSoftwareInstallResponse
				s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/device/%s/software/install/%d", install.deviceToken, install.titleID),
					&fleetSelfServiceSoftwareInstallRequest{}, http.StatusAccepted, &ssInstallResp)
			} else {
				installResp = installSoftwareResponse{}
				s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/install", installHost.ID, titleID),
					&installSoftwareRequest{}, http.StatusAccepted, &installResp)
			}
			countResp = countHostsResponse{}
			s.DoJSON("GET", "/api/latest/fleet/hosts/count", nil, http.StatusOK, &countResp, "software_status", "pending", "team_id",
				fmt.Sprint(team.ID), "software_title_id", fmt.Sprint(titleID))
			require.Equal(t, 1, countResp.Count)

			// send an idle request to grab the command uuid
			cmd, err = mdmClient.Idle()
			require.NoError(t, err)
			var fullCmd micromdm.CommandPayload
			require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd))
			installCmdUUID = cmd.CommandUUID

			if install.deviceToken != "" {
				var cmdResResp getMDMCommandResultsResponse
				res := s.DoRawNoAuth("GET", fmt.Sprintf("/api/latest/fleet/device/%s/software/commands/%s/results", install.deviceToken, installCmdUUID), nil, http.StatusOK)
				err = json.NewDecoder(res.Body).Decode(&cmdResResp)
				require.NoError(t, err)
				require.Len(t, cmdResResp.Results, 0)
			}

			// Get pending activity
			var hostActivitiesResp listHostUpcomingActivitiesResponse
			s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/activities/upcoming", installHost.ID),
				nil, http.StatusOK, &hostActivitiesResp)
			activitiesToString := func(activities []*fleet.UpcomingActivity) []string {
				var res []string
				for _, activity := range activities {
					res = append(res, fmt.Sprintf("%+v", activity))
				}
				return res
			}
			require.Len(t, hostActivitiesResp.Activities, 1, "got activities: %v", activitiesToString(hostActivitiesResp.Activities))
			require.Equal(t, hostActivitiesResp.Activities[0].Type, fleet.ActivityInstalledAppStoreApp{}.ActivityName())
			require.EqualValues(t, 1, hostActivitiesResp.Count)
			require.JSONEq(
				t,
				fmt.Sprintf(
					`{"host_id": %d, "host_display_name": "%s", "software_title": "%s", "app_store_id": "%s", "command_uuid": "%s", "status": "%s", "self_service": %v}`,
					installHost.ID,
					installHost.DisplayName(),
					app.Name,
					app.AdamID,
					installCmdUUID,
					fleet.SoftwareInstallPending,
					install.deviceToken != "",
				),
				string(*hostActivitiesResp.Activities[0].Details),
			)

			// Simulate successful installation on the host
			cmd, err = mdmClient.Acknowledge(cmd.CommandUUID)
			require.NoError(t, err)

			// Process InstalledApplicationList command for install verification
			s.runWorker()

			cmd, err = mdmClient.Idle()
			require.NoError(t, err)
			for cmd != nil {
				var fullCmd micromdm.CommandPayload
				switch cmd.Command.RequestType {
				case "InstalledApplicationList":
					require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd))
					cmd, err = mdmClient.AcknowledgeInstalledApplicationList(mdmClient.UUID, cmd.CommandUUID, []fleet.Software{{Name: app.Name, BundleIdentifier: app.BundleIdentifier, Version: app.LatestVersion, Installed: true}})
					require.NoError(t, err)
				default:
					require.Fail(t, "unexpected command type", cmd.Command.RequestType)
				}
			}

			if install.deviceToken != "" {
				var cmdResResp getMDMCommandResultsResponse
				res := s.DoRawNoAuth("GET", fmt.Sprintf("/api/latest/fleet/device/%s/software/commands/%s/results", install.deviceToken, installCmdUUID), nil, http.StatusOK)
				err = json.NewDecoder(res.Body).Decode(&cmdResResp)
				require.NoError(t, err)
				require.Len(t, cmdResResp.Results, 1)
				require.Equal(t, "Acknowledged", cmdResResp.Results[0].Status)

				s.DoRawNoAuth("GET", fmt.Sprintf("/api/latest/fleet/device/%s/software/commands/foobar/results", install.deviceToken), nil, http.StatusNotFound)
				s.DoRawNoAuth("GET", fmt.Sprintf("/api/latest/fleet/device/foobar/software/commands/%s/results", installCmdUUID), nil, http.StatusNotFound)
			}

			listResp = listHostsResponse{}
			s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listResp, "software_status", "installed", "team_id",
				fmt.Sprint(team.ID), "software_title_id", fmt.Sprint(titleID))
			require.Len(t, listResp.Hosts, install.hostCount)
			countResp = countHostsResponse{}
			s.DoJSON("GET", "/api/latest/fleet/hosts/count", nil, http.StatusOK, &countResp, "software_status", "installed", "team_id",
				fmt.Sprint(team.ID), "software_title_id", fmt.Sprint(titleID))
			require.Equal(t, install.hostCount, countResp.Count)

			s.lastActivityMatches(
				fleet.ActivityInstalledAppStoreApp{}.ActivityName(),
				fmt.Sprintf(
					`{"host_id": %d, "host_display_name": "%s", "software_title": "%s", "app_store_id": "%s", "command_uuid": "%s", "status": "%s", "self_service": %v, "policy_id": null, "policy_name": null}`,
					installHost.ID,
					installHost.DisplayName(),
					app.Name,
					app.AdamID,
					installCmdUUID,
					fleet.SoftwareInstalled,
					install.deviceToken != "",
				),
				0,
			)

			// Check list host software
			getHostSw = getHostSoftwareResponse{}
			s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/software", installHost.ID), nil, http.StatusOK, &getHostSw)
			require.Len(t, getHostSw.Software, install.hostCount+install.extraAvailable)
			var foundInstalledApp bool
			for index := range getHostSw.Software {
				got1 = getHostSw.Software[index]
				if got1.Status != nil {
					require.Equal(t, got1.Name, app.Name)
					require.NotNil(t, got1.AppStoreApp)
					require.Equal(t, got1.AppStoreApp.AppStoreID, app.AdamID)
					require.Equal(t, got1.IconUrl, ptr.String(app.IconURL))
					require.Empty(t, got1.AppStoreApp.Name) // Name is only present for installer packages
					require.Equal(t, got1.AppStoreApp.Version, app.LatestVersion)
					require.Equal(t, *got1.Status, fleet.SoftwareInstalled)
					require.Equal(t, got1.AppStoreApp.LastInstall.CommandUUID, installCmdUUID)
					require.NotNil(t, got1.AppStoreApp.LastInstall.InstalledAt)
					foundInstalledApp = true
				}
			}
			assert.True(t, foundInstalledApp)
		})
	}

	// Attempt (and fail) to self-service install iPad and iOS titles
	var ssInstallResp submitSelfServiceSoftwareInstallResponse
	s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/device/%s/software/install/%d", selfServiceToken, iPadOSTitleID), &fleetSelfServiceSoftwareInstallRequest{},
		http.StatusBadRequest, &ssInstallResp)
	s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/device/%s/software/install/%d", selfServiceToken, iOSTitleID), &fleetSelfServiceSoftwareInstallRequest{},
		http.StatusBadRequest, &ssInstallResp)

	// Delete VPP token and check that it's not appearing anymore

	s.Do("DELETE", fmt.Sprintf("/api/latest/fleet/vpp_tokens/%d", validToken.Token.ID), &deleteVPPTokenResponse{}, http.StatusNoContent)
	s.DoJSON("GET", "/api/latest/fleet/vpp_tokens", &getVPPTokensRequest{}, http.StatusOK, &resp)
}

func (s *integrationMDMTestSuite) TestNoTeamVPPAppIcons() {
	t := s.T()
	s.setSkipWorkerJobs(t)

	// Valid token
	orgName := "Fleet Device Management Inc."
	location := "Fleet Location One"
	token := "mycooltoken"
	expTime := time.Now().Add(200 * time.Hour).UTC().Round(time.Second)
	expDate := expTime.Format(fleet.VPPTimeFormat)
	tokenJSON := fmt.Sprintf(`{"expDate":"%s","token":"%s","orgName":"%s"}`, expDate, token, orgName)
	t.Setenv("FLEET_DEV_VPP_URL", s.appleVPPConfigSrv.URL)
	var validToken uploadVPPTokenResponse
	s.uploadDataViaForm("/api/latest/fleet/vpp_tokens", "token", "token.vpptoken", []byte(base64.StdEncoding.EncodeToString([]byte(tokenJSON))), http.StatusAccepted, "", &validToken)

	// Get the token
	var resp getVPPTokensResponse
	s.DoJSON("GET", "/api/latest/fleet/vpp_tokens", &getVPPTokensRequest{}, http.StatusOK, &resp)
	require.NoError(t, resp.Err)
	require.Len(t, resp.Tokens, 1)
	require.Equal(t, orgName, resp.Tokens[0].OrgName)
	require.Equal(t, location, resp.Tokens[0].Location)
	require.Equal(t, expTime, resp.Tokens[0].RenewDate)

	var resPatchVPP patchVPPTokensTeamsResponse
	s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/vpp_tokens/%d/teams", resp.Tokens[0].ID), patchVPPTokensTeamsRequest{TeamIDs: []uint{fleet.PolicyNoTeamID}}, http.StatusOK, &resPatchVPP)

	macOSApp := fleet.VPPApp{
		VPPAppTeam: fleet.VPPAppTeam{
			VPPAppID: fleet.VPPAppID{
				AdamID:   "1",
				Platform: fleet.MacOSPlatform,
			},
		},
		Name:             "App 1",
		BundleIdentifier: "a-1",
		IconURL:          "https://example.com/images/1",
		LatestVersion:    "1.0.0",
	}

	// Insert/deletion flow for macOS app
	// Add an app store app to team no team
	addedApp := macOSApp
	var addAppResp addAppStoreAppResponse
	s.DoJSON("POST", "/api/latest/fleet/software/app_store_apps", &addAppStoreAppRequest{TeamID: ptr.Uint(fleet.PolicyNoTeamID), AppStoreID: addedApp.AdamID, SelfService: true}, http.StatusOK, &addAppResp)

	// list the software titles, to get the title id of the VPP app
	var listSw listSoftwareTitlesResponse
	s.DoJSON("GET", "/api/latest/fleet/software/titles", nil, http.StatusOK, &listSw, "team_id", fmt.Sprint(fleet.PolicyNoTeamID), "available_for_install", "true")
	require.Len(t, listSw.SoftwareTitles, 1)
	require.True(t, *listSw.SoftwareTitles[0].AppStoreApp.SelfService)
	macOSTitleID := listSw.SoftwareTitles[0].ID

	_, err := s.ds.CreateOrUpdateSoftwareTitleIcon(context.Background(), &fleet.UploadSoftwareTitleIconPayload{
		TitleID:   macOSTitleID,
		Filename:  "icon.png",
		TeamID:    fleet.PolicyNoTeamID,
		StorageID: "icon_storage_id",
	})
	require.NoError(t, err)

	var count int
	mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
		return sqlx.GetContext(context.Background(), q, &count, `SELECT COUNT(1) FROM software_title_icons WHERE team_id = ? and software_title_id = ?`, fleet.PolicyNoTeamID, macOSTitleID)
	})
	require.Equal(t, 1, count)

	// delete the app store app for team no team
	s.Do("DELETE", fmt.Sprintf("/api/latest/fleet/software/titles/%d/available_for_install", macOSTitleID), nil, http.StatusNoContent,
		"team_id",
		fmt.Sprint(fleet.PolicyNoTeamID))
	s.lastActivityMatches(fleet.ActivityDeletedAppStoreApp{}.ActivityName(),
		fmt.Sprintf(`{"team_name": null, "software_title": "%s", "app_store_id": "%s", "software_icon_url": "/api/latest/fleet/software/titles/%d/icon?team_id=%d", "team_id": %d, "platform": "%s"}`,
			addedApp.Name, addedApp.AdamID, macOSTitleID, fleet.PolicyNoTeamID, fleet.PolicyNoTeamID, addedApp.Platform), 0)

	mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
		return sqlx.GetContext(context.Background(), q, &count, `SELECT COUNT(1) FROM software_title_icons WHERE team_id = ? and software_title_id = ?`, fleet.PolicyNoTeamID, macOSTitleID)
	})
	require.Equal(t, 0, count)

	// Delete VPP token and check that it's not appearing anymore

	s.Do("DELETE", fmt.Sprintf("/api/latest/fleet/vpp_tokens/%d", validToken.Token.ID), &deleteVPPTokenResponse{}, http.StatusNoContent)
	s.DoJSON("GET", "/api/latest/fleet/vpp_tokens", &getVPPTokensRequest{}, http.StatusOK, &resp)
}

func (s *integrationMDMTestSuite) TestVPPAppPolicyAutomation() {
	t := s.T()
	ctx := context.Background()

	activitiesToString := func(activities []*fleet.UpcomingActivity) []string {
		var res []string
		for _, activity := range activities {
			res = append(res, fmt.Sprintf("%+v", activity))
		}
		return res
	}

	// Set up VPP token
	orgName := "Fleet Device Management Inc."
	token := "mycooltoken"
	expTime := time.Now().Add(200 * time.Hour).UTC().Round(time.Second)
	expDate := expTime.Format(fleet.VPPTimeFormat)
	tokenJSON := fmt.Sprintf(`{"expDate":"%s","token":"%s","orgName":"%s"}`, expDate, token, orgName)
	t.Setenv("FLEET_DEV_VPP_URL", s.appleVPPConfigSrv.URL)
	var validToken uploadVPPTokenResponse
	s.uploadDataViaForm("/api/latest/fleet/vpp_tokens", "token", "token.vpptoken", []byte(base64.StdEncoding.EncodeToString([]byte(tokenJSON))), http.StatusAccepted, "", &validToken)

	// Get the token
	var resp getVPPTokensResponse
	s.DoJSON("GET", "/api/latest/fleet/vpp_tokens", &getVPPTokensRequest{}, http.StatusOK, &resp)
	require.NoError(t, resp.Err)

	// Create a team
	var newTeamResp teamResponse
	s.DoJSON("POST", "/api/latest/fleet/teams", &createTeamRequest{TeamPayload: fleet.TeamPayload{Name: ptr.String("Team 1")}}, http.StatusOK, &newTeamResp)
	team := newTeamResp.Team

	// Create a couple of hosts
	orbitHost := createOrbitEnrolledHost(t, "darwin", "nonmdm", s.ds)
	mdmHost, mdmDevice := createHostThenEnrollMDM(s.ds, s.server.URL, t)
	setOrbitEnrollment(t, mdmHost, s.ds)
	s.runWorker()
	checkInstallFleetdCommandSent(t, mdmDevice, true)
	mdmHost2, mdmDevice2 := createHostThenEnrollMDM(s.ds, s.server.URL, t)
	s.runWorker()
	checkInstallFleetdCommandSent(t, mdmDevice2, true)
	key := setOrbitEnrollment(t, mdmHost2, s.ds)
	mdmHost2.OrbitNodeKey = &key
	selfServiceHost, selfServiceDevice := createHostThenEnrollMDM(s.ds, s.server.URL, t)
	setOrbitEnrollment(t, selfServiceHost, s.ds)
	selfServiceToken := "selfservicetoken"
	updateDeviceTokenForHost(t, s.ds, selfServiceHost.ID, selfServiceToken)
	s.appleVPPConfigSrvConfig.SerialNumbers = append(s.appleVPPConfigSrvConfig.SerialNumbers, selfServiceDevice.SerialNumber)

	// Add serial number to our fake Apple server
	s.appleVPPConfigSrvConfig.SerialNumbers = append(s.appleVPPConfigSrvConfig.SerialNumbers, mdmHost.HardwareSerial, mdmHost2.HardwareSerial)
	s.Do("POST", "/api/latest/fleet/hosts/transfer",
		&addHostsToTeamRequest{HostIDs: []uint{mdmHost.ID, mdmHost2.ID, orbitHost.ID, selfServiceHost.ID}, TeamID: &team.ID}, http.StatusOK)

	// Associate team to the VPP token.
	var resPatchVPP patchVPPTokensTeamsResponse
	s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/vpp_tokens/%d/teams", resp.Tokens[0].ID), patchVPPTokensTeamsRequest{TeamIDs: []uint{team.ID}}, http.StatusOK, &resPatchVPP)

	var createLabelResp createLabelResponse
	s.DoJSON("POST", "/api/latest/fleet/labels", &fleet.LabelPayload{Name: "label1" + t.Name(), Hosts: []string{mdmHost.HardwareSerial}}, http.StatusOK, &createLabelResp)
	l1 := createLabelResp.Label
	require.NotNil(t, l1)

	s.DoJSON("POST", "/api/latest/fleet/labels", &fleet.LabelPayload{Name: "label2" + t.Name(), Hosts: []string{mdmHost2.HardwareSerial}}, http.StatusOK, &createLabelResp)
	l2 := createLabelResp.Label

	// Get list of VPP apps from "Apple"
	// We're passing team 1 here, but we haven't added any app store apps to that team, so we get
	// back all available apps in our VPP location.
	var appResp getAppStoreAppsResponse
	s.DoJSON("GET", "/api/latest/fleet/software/app_store_apps", &getAppStoreAppsRequest{}, http.StatusOK, &appResp, "team_id",
		fmt.Sprint(team.ID))
	require.NoError(t, appResp.Err)

	macOSApp := fleet.VPPApp{
		VPPAppTeam: fleet.VPPAppTeam{
			VPPAppID: fleet.VPPAppID{
				AdamID:   "1",
				Platform: fleet.MacOSPlatform,
			},
		},
		Name:             "App 1",
		BundleIdentifier: "a-1",
		IconURL:          "https://example.com/images/1",
		LatestVersion:    "1.0.0",
	}
	iPadOSApp := fleet.VPPApp{
		VPPAppTeam: fleet.VPPAppTeam{
			VPPAppID: fleet.VPPAppID{
				AdamID:   "2",
				Platform: fleet.IPadOSPlatform,
			},
		},
		Name:             "App 2",
		BundleIdentifier: "b-2",
		IconURL:          "https://example.com/images/2",
		LatestVersion:    "2.0.0",
	}
	iOSApp := fleet.VPPApp{
		VPPAppTeam: fleet.VPPAppTeam{
			VPPAppID: fleet.VPPAppID{
				AdamID:   "2",
				Platform: fleet.IOSPlatform,
			},
		},
		Name:             "App 2",
		BundleIdentifier: "b-2",
		IconURL:          "https://example.com/images/2",
		LatestVersion:    "2.0.0",
	}
	expectedApps := []*fleet.VPPApp{
		&macOSApp,
		&iPadOSApp,
		&iOSApp,
		{
			VPPAppTeam: fleet.VPPAppTeam{
				VPPAppID: fleet.VPPAppID{
					AdamID:   "2",
					Platform: fleet.MacOSPlatform,
				},
			},
			Name:             "App 2",
			BundleIdentifier: "b-2",
			IconURL:          "https://example.com/images/2",
			LatestVersion:    "2.0.0",
		},
		{
			VPPAppTeam: fleet.VPPAppTeam{
				VPPAppID: fleet.VPPAppID{
					AdamID:   "3",
					Platform: fleet.IPadOSPlatform,
				},
			},
			Name:             "App 3",
			BundleIdentifier: "c-3",
			IconURL:          "https://example.com/images/3",
			LatestVersion:    "3.0.0",
		},
	}
	assert.ElementsMatch(t, expectedApps, appResp.AppStoreApps)

	getSoftwareTitleIDFromApp := func(app *fleet.VPPApp) uint {
		var titleID uint
		mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
			return sqlx.GetContext(ctx, q, &titleID, `SELECT title_id FROM vpp_apps WHERE adam_id = ? AND platform = ?;`, app.AdamID, app.Platform)
		})

		return titleID
	}

	// Insert/deletion flow for macOS app
	// Add an app store app to team 1
	addedApp := expectedApps[0]
	var addedMacOSApp addAppStoreAppResponse
	s.DoJSON("POST", "/api/latest/fleet/software/app_store_apps", &addAppStoreAppRequest{TeamID: &team.ID, Platform: addedApp.Platform, AppStoreID: addedApp.AdamID, SelfService: true, LabelsIncludeAny: []string{l1.Name}}, http.StatusOK, &addedMacOSApp)

	// list the software titles for that team, to get the title id of the VPP app
	var listSw listSoftwareTitlesResponse
	s.DoJSON("GET", "/api/latest/fleet/software/titles", nil, http.StatusOK, &listSw, "team_id", fmt.Sprint(team.ID), "available_for_install", "true")
	require.Len(t, listSw.SoftwareTitles, 1)
	require.True(t, *listSw.SoftwareTitles[0].AppStoreApp.SelfService)
	macOSTitleID := listSw.SoftwareTitles[0].ID

	// Insert iPadOS app
	addedApp = expectedApps[1]
	addedIOSApp := addAppStoreAppResponse{}
	s.DoJSON("POST", "/api/latest/fleet/software/app_store_apps",
		&addAppStoreAppRequest{TeamID: &team.ID, AppStoreID: addedApp.AdamID, Platform: addedApp.Platform},
		http.StatusOK, &addedIOSApp)

	// Add all apps to the team
	appSelfService := expectedApps[0]
	// Add app 1 as self-service
	addedMacOSApp = addAppStoreAppResponse{}
	s.DoJSON("POST", "/api/latest/fleet/software/app_store_apps",
		&addAppStoreAppRequest{TeamID: &team.ID, AppStoreID: appSelfService.AdamID, Platform: appSelfService.Platform, SelfService: true, LabelsIncludeAny: []string{l1.Name}},
		http.StatusOK, &addedMacOSApp)
	// Add remaining as non-self-service
	for _, app := range expectedApps[1:] {
		addedMacOSApp = addAppStoreAppResponse{}
		s.DoJSON("POST", "/api/latest/fleet/software/app_store_apps",
			&addAppStoreAppRequest{TeamID: &team.ID, AppStoreID: app.AdamID, Platform: app.Platform},
			http.StatusOK, &addedMacOSApp)
		s.lastActivityMatches(
			fleet.ActivityAddedAppStoreApp{}.ActivityName(),
			fmt.Sprintf(`{"team_name": "%s", "software_title": "%s", "software_title_id": %d, "app_store_id": "%s", "team_id": %d, "platform": "%s", "self_service": false}`, team.Name,
				app.Name, getSoftwareTitleIDFromApp(app), app.AdamID, team.ID, app.Platform),
			0,
		)
		listSw = listSoftwareTitlesResponse{}
		s.DoJSON("GET", "/api/latest/fleet/software/titles", nil, http.StatusOK, &listSw, "team_id", fmt.Sprint(team.ID),
			"available_for_install", "true")
	}

	listSw = listSoftwareTitlesResponse{}
	s.DoJSON("GET", "/api/latest/fleet/software/titles", nil, http.StatusOK, &listSw, "team_id", fmt.Sprint(team.ID), "available_for_install", "true")
	require.Len(t, listSw.SoftwareTitles, len(expectedApps))
	iOSTitleID := uint(99999)

	policy0Team1, err := s.ds.NewTeamPolicy(ctx, team.ID, nil, fleet.PolicyPayload{
		Name:     "policy0Team1",
		Query:    "SELECT 1;",
		Platform: "darwin",
	})
	require.NoError(t, err)
	policy1Team1, err := s.ds.NewTeamPolicy(ctx, team.ID, nil, fleet.PolicyPayload{
		Name:     "policy1Team1",
		Query:    "SELECT 1;",
		Platform: "darwin",
	})
	require.NoError(t, err)
	policy2Team1, err := s.ds.NewTeamPolicy(ctx, team.ID, nil, fleet.PolicyPayload{ // will be set up with same VPP app
		Name:     "policy2Team1",
		Query:    "SELECT 1;",
		Platform: "darwin",
	})
	require.NoError(t, err)

	policy3Team1, err := s.ds.NewTeamPolicy(ctx, team.ID, nil, fleet.PolicyPayload{ // will be set up with same VPP app + a script
		Name:     "policy3Team1",
		Query:    "SELECT 1;",
		Platform: "darwin",
	})
	require.NoError(t, err)

	savedTmScript, err := s.ds.NewScript(ctx, &fleet.Script{
		TeamID:         &team.ID,
		Name:           "team_script.sh",
		ScriptContents: "echo 'team'",
	})
	require.NoError(t, err)

	mtplr := modifyTeamPolicyResponse{}
	s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d/policies/%d", team.ID, policy1Team1.ID), modifyTeamPolicyRequest{
		ModifyPolicyPayload: fleet.ModifyPolicyPayload{
			SoftwareTitleID: optjson.Any[uint]{Set: true, Valid: true, Value: iOSTitleID},
		},
	}, http.StatusBadRequest, &mtplr)
	s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d/policies/%d", team.ID, policy1Team1.ID), modifyTeamPolicyRequest{
		ModifyPolicyPayload: fleet.ModifyPolicyPayload{
			SoftwareTitleID: optjson.Any[uint]{Set: true, Valid: true, Value: macOSTitleID},
		},
	}, http.StatusOK, &mtplr)

	s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d/policies/%d", team.ID, policy3Team1.ID), modifyTeamPolicyRequest{
		ModifyPolicyPayload: fleet.ModifyPolicyPayload{
			SoftwareTitleID: optjson.Any[uint]{Set: true, Valid: true, Value: macOSTitleID},
			ScriptID:        optjson.Any[uint]{Set: true, Valid: true, Value: savedTmScript.ID},
		},
	}, http.StatusOK, &mtplr)

	titleResponse := getSoftwareTitleResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d", macOSTitleID), getSoftwareTitleRequest{
		TeamID: &team.ID,
	}, http.StatusOK, &titleResponse)
	require.Len(t, titleResponse.SoftwareTitle.AppStoreApp.AutomaticInstallPolicies, 2)
	require.Equal(t, titleResponse.SoftwareTitle.AppStoreApp.AutomaticInstallPolicies[0].ID, policy1Team1.ID)

	s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d/policies/%d", team.ID, policy2Team1.ID), modifyTeamPolicyRequest{
		ModifyPolicyPayload: fleet.ModifyPolicyPayload{
			SoftwareTitleID: optjson.Any[uint]{Set: true, Valid: true, Value: macOSTitleID},
		},
	}, http.StatusOK, &mtplr)
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d", macOSTitleID), getSoftwareTitleRequest{
		TeamID: &team.ID,
	}, http.StatusOK, &titleResponse)
	require.Len(t, titleResponse.SoftwareTitle.AppStoreApp.AutomaticInstallPolicies, 3)

	// add a non-macOS host
	newHost := func(name string, teamID *uint, platform string) *fleet.Host {
		h, err := s.ds.NewHost(ctx, &fleet.Host{
			DetailUpdatedAt: time.Now(),
			LabelUpdatedAt:  time.Now(),
			PolicyUpdatedAt: time.Now(),
			SeenTime:        time.Now().Add(-1 * time.Minute),
			OsqueryHostID:   ptr.String(t.Name() + name),
			NodeKey:         ptr.String(t.Name() + name),
			UUID:            uuid.New().String(),
			Hostname:        fmt.Sprintf("%s.%s.local", name, t.Name()),
			Platform:        platform,
			TeamID:          teamID,
		})
		require.NoError(t, err)
		return h
	}
	newFleetdHost := func(name string, teamID *uint, platform string) *fleet.Host {
		h := newHost(name, teamID, platform)
		orbitKey := setOrbitEnrollment(t, h, s.ds)
		h.OrbitNodeKey = &orbitKey
		return h
	}

	host3 := newFleetdHost("host3", &team.ID, "ubuntu")

	// setting/unsetting is covered by the other software automation integration test

	// We use DoJSONWithoutAuth for distributed/write because we want the requests to not have the
	// current user's "Authorization: Bearer <API_TOKEN>" header.

	// host3 failure should not queue an install due to mismatched platform
	distributedResp := submitDistributedQueryResultsResponse{}
	s.DoJSONWithoutAuth("POST", "/api/osquery/distributed/write", genDistributedReqWithPolicyResults(
		host3,
		map[uint]*bool{
			policy1Team1.ID: ptr.Bool(false),
		},
	), http.StatusOK, &distributedResp)
	var countResp countHostsResponse
	s.DoJSON("GET", "/api/latest/fleet/hosts/count", nil, http.StatusOK, &countResp, "software_status", "pending", "team_id",
		fmt.Sprint(team.ID), "software_title_id", fmt.Sprint(macOSTitleID))
	require.Equal(t, 0, countResp.Count)

	// non-MDM host failure should not queue an install due to lack of MDM
	s.DoJSONWithoutAuth("POST", "/api/osquery/distributed/write", genDistributedReqWithPolicyResults(
		orbitHost,
		map[uint]*bool{
			policy1Team1.ID: ptr.Bool(false),
		},
	), http.StatusOK, &distributedResp)
	s.DoJSON("GET", "/api/latest/fleet/hosts/count", nil, http.StatusOK, &countResp, "software_status", "pending", "team_id",
		fmt.Sprint(team.ID), "software_title_id", fmt.Sprint(macOSTitleID))
	require.Equal(t, 0, countResp.Count)

	// MDM host passing policy should not queue install, nor should failing non-VPP policy
	s.DoJSONWithoutAuth("POST", "/api/osquery/distributed/write", genDistributedReqWithPolicyResults(
		mdmHost,
		map[uint]*bool{
			policy0Team1.ID: ptr.Bool(false),
			policy1Team1.ID: ptr.Bool(true),
		},
	), http.StatusOK, &distributedResp)
	s.DoJSON("GET", "/api/latest/fleet/hosts/count", nil, http.StatusOK, &countResp, "software_status", "pending", "team_id",
		fmt.Sprint(team.ID), "software_title_id", fmt.Sprint(macOSTitleID))
	require.Equal(t, 0, countResp.Count)

	// MDM host failing policy should queue install (adam ID 1 for macOS) and it will be "activated"
	// in the queue as there is nothing pending for that host.
	s.DoJSONWithoutAuth("POST", "/api/osquery/distributed/write", genDistributedReqWithPolicyResults(
		mdmHost,
		map[uint]*bool{
			policy1Team1.ID: ptr.Bool(false),
		},
	), http.StatusOK, &distributedResp)
	s.DoJSON("GET", "/api/latest/fleet/hosts/count", nil, http.StatusOK, &countResp, "software_status", "pending", "team_id",
		fmt.Sprint(team.ID), "software_title_id", fmt.Sprint(macOSTitleID))
	require.Equal(t, 1, countResp.Count)

	// App is out of scope for mdmHost2, so we do not enqueue a software install
	// but we do enqueue a script (will be "activated" in the queue as there is nothing
	// pending for that host).
	s.DoJSONWithoutAuth("POST", "/api/osquery/distributed/write", genDistributedReqWithPolicyResults(
		mdmHost2,
		map[uint]*bool{
			policy3Team1.ID: ptr.Bool(false),
		},
	), http.StatusOK, &distributedResp)
	s.DoJSON("GET", "/api/latest/fleet/hosts/count", nil, http.StatusOK, &countResp, "software_status", "pending", "team_id",
		fmt.Sprint(team.ID), "software_title_id", fmt.Sprint(macOSTitleID))
	require.Equal(t, 1, countResp.Count)

	var getHostResp getHostResponse
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", mdmHost2.ID), nil, http.StatusOK, &getHostResp, "exclude_software", "true")
	require.Equal(t, getHostResp.Host.ID, mdmHost2.ID)
	require.NotNil(t, getHostResp.Host.Policies)
	require.Len(t, *getHostResp.Host.Policies, 4)
	policySeen := false
	for _, p := range *getHostResp.Host.Policies {
		if p.Name == policy3Team1.Name {
			require.Empty(t, p.Response)
			policySeen = true
		}
	}
	require.True(t, policySeen)

	// Validate that orbit got a notif (and get the exec ID for the script)
	var orbitResp orbitGetConfigResponse
	s.DoJSON("POST", "/api/fleet/orbit/config",
		json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *mdmHost2.OrbitNodeKey)),
		http.StatusOK, &orbitResp)
	require.Len(t, orbitResp.Notifications.PendingScriptExecutionIDs, 1)
	scriptExecID := orbitResp.Notifications.PendingScriptExecutionIDs[0]

	var hostActivitiesResp listHostUpcomingActivitiesResponse
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/activities/upcoming", mdmHost2.ID),
		nil, http.StatusOK, &hostActivitiesResp)

	require.Len(t, hostActivitiesResp.Activities, 1, "got activities: %v", activitiesToString(hostActivitiesResp.Activities))
	assert.Equal(t, hostActivitiesResp.Activities[0].Type, fleet.ActivityTypeRanScript{}.ActivityName())
	assert.True(t, hostActivitiesResp.Activities[0].FleetInitiated)
	assert.EqualValues(t, 1, hostActivitiesResp.Count)
	assert.JSONEq(
		t,
		fmt.Sprintf(
			`{"host_id": %d, "host_display_name": "%s", "script_name": "%s", "async": true, "policy_id": %d, "policy_name": "%s", "script_execution_id": "%s", "batch_execution_id": null}`,
			mdmHost2.ID,
			mdmHost2.DisplayName(),
			savedTmScript.Name,
			policy3Team1.ID,
			policy3Team1.Name,
			scriptExecID,
		),
		string(*hostActivitiesResp.Activities[0].Details),
	)

	var orbitPostScriptResp orbitPostScriptResultResponse
	s.DoJSON("POST", "/api/fleet/orbit/scripts/result",
		json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q, "execution_id": %q, "exit_code": 0, "output": "ok"}`, *mdmHost2.OrbitNodeKey, scriptExecID)),
		http.StatusOK, &orbitPostScriptResp)

	s.lastActivityMatchesExtended(
		fleet.ActivityTypeRanScript{}.ActivityName(),
		fmt.Sprintf(
			`{"host_id": %d, "host_display_name": %q, "script_name": %q, "script_execution_id": %q, "async": true, "policy_id": %d, "policy_name": "%s", "batch_execution_id": null}`,
			mdmHost2.ID, mdmHost2.DisplayName(), savedTmScript.Name, scriptExecID, policy3Team1.ID, policy3Team1.Name,
		),
		0,
		ptr.Bool(true),
	)

	// Update the app to exclude any with l2. We should not enqueue an install here because mdmHost2
	// has l2 (but it will re-enqueue the script for execution, immediately "activated")
	updateAppReq := &updateAppStoreAppRequest{TeamID: &team.ID, SelfService: ptr.Bool(false), LabelsExcludeAny: []string{l2.Name}}
	s.Do("PATCH", fmt.Sprintf("/api/latest/fleet/software/titles/%d/app_store_app", macOSTitleID), updateAppReq, http.StatusOK)

	s.DoJSONWithoutAuth("POST", "/api/osquery/distributed/write", genDistributedReqWithPolicyResults(
		mdmHost2,
		map[uint]*bool{
			policy3Team1.ID: ptr.Bool(false),
		},
	), http.StatusOK, &distributedResp)
	s.DoJSON("GET", "/api/latest/fleet/hosts/count", nil, http.StatusOK, &countResp, "software_status", "pending", "team_id",
		fmt.Sprint(team.ID), "software_title_id", fmt.Sprint(macOSTitleID))
	require.Equal(t, 1, countResp.Count)

	// Check that the policy shows up as "nil" status
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", mdmHost2.ID), nil, http.StatusOK, &getHostResp, "exclude_software", "true")
	require.Equal(t, getHostResp.Host.ID, mdmHost2.ID)
	require.NotNil(t, getHostResp.Host.Policies)
	require.Len(t, *getHostResp.Host.Policies, 4)
	policySeen = false
	for _, p := range *getHostResp.Host.Policies {
		if p.Name == policy3Team1.Name {
			require.Empty(t, p.Response)
			policySeen = true
		}
	}
	require.True(t, policySeen)

	s.DoJSON("POST", "/api/fleet/orbit/config",
		json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *mdmHost2.OrbitNodeKey)),
		http.StatusOK, &orbitResp)
	require.Len(t, orbitResp.Notifications.PendingScriptExecutionIDs, 1)
	scriptExecID = orbitResp.Notifications.PendingScriptExecutionIDs[0]

	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/activities/upcoming", mdmHost2.ID),
		nil, http.StatusOK, &hostActivitiesResp)

	require.Len(t, hostActivitiesResp.Activities, 1, "got activities: %v", activitiesToString(hostActivitiesResp.Activities))
	assert.Equal(t, hostActivitiesResp.Activities[0].Type, fleet.ActivityTypeRanScript{}.ActivityName())
	assert.EqualValues(t, 1, hostActivitiesResp.Count)
	assert.JSONEq(
		t,
		fmt.Sprintf(
			`{"host_id": %d, "host_display_name": "%s", "script_name": "%s", "async": true, "policy_id": %d, "policy_name": "%s", "script_execution_id": "%s", "batch_execution_id": null}`,
			mdmHost2.ID,
			mdmHost2.DisplayName(),
			savedTmScript.Name,
			policy3Team1.ID,
			policy3Team1.Name,
			scriptExecID,
		),
		string(*hostActivitiesResp.Activities[0].Details),
	)

	s.DoJSON("POST", "/api/fleet/orbit/scripts/result",
		json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q, "execution_id": %q, "exit_code": 0, "output": "ok"}`, *mdmHost2.OrbitNodeKey, scriptExecID)),
		http.StatusOK, &orbitPostScriptResp)

	s.lastActivityMatchesExtended(
		fleet.ActivityTypeRanScript{}.ActivityName(),
		fmt.Sprintf(
			`{"host_id": %d, "host_display_name": %q, "script_name": %q, "script_execution_id": %q, "async": true, "policy_id": %d, "policy_name": "%s", "batch_execution_id": null}`,
			mdmHost2.ID, mdmHost2.DisplayName(), savedTmScript.Name, scriptExecID, policy3Team1.ID, policy3Team1.Name,
		),
		0,
		ptr.Bool(true),
	)

	// Update the app to include any with l1. We should now enqueue an install as the app is in scope
	// (in addition to the script execution, which will be the only one "activated").
	updateAppReq = &updateAppStoreAppRequest{TeamID: &team.ID, SelfService: ptr.Bool(false), LabelsIncludeAny: []string{l1.Name, l2.Name}}
	s.Do("PATCH", fmt.Sprintf("/api/latest/fleet/software/titles/%d/app_store_app", macOSTitleID), updateAppReq, http.StatusOK)

	s.DoJSONWithoutAuth("POST", "/api/osquery/distributed/write", genDistributedReqWithPolicyResults(
		mdmHost2,
		map[uint]*bool{
			policy3Team1.ID: ptr.Bool(false),
		},
	), http.StatusOK, &distributedResp)
	s.DoJSON("GET", "/api/latest/fleet/hosts/count", nil, http.StatusOK, &countResp, "software_status", "pending", "team_id",
		fmt.Sprint(team.ID), "software_title_id", fmt.Sprint(macOSTitleID))
	require.Equal(t, 2, countResp.Count)

	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", mdmHost2.ID), nil, http.StatusOK, &getHostResp, "exclude_software", "true")
	require.Equal(t, getHostResp.Host.ID, mdmHost2.ID)
	require.NotNil(t, getHostResp.Host.Policies)
	require.Len(t, *getHostResp.Host.Policies, 4)
	policySeen = false
	for _, p := range *getHostResp.Host.Policies {
		if p.Name == policy3Team1.Name {
			require.Equal(t, "fail", p.Response)
			policySeen = true
		}
	}
	require.True(t, policySeen)

	// MDM host failing policy should not queue another install while install is pending
	s.DoJSONWithoutAuth("POST", "/api/osquery/distributed/write", genDistributedReqWithPolicyResults(
		mdmHost,
		map[uint]*bool{
			policy1Team1.ID: ptr.Bool(false),
			policy2Team1.ID: ptr.Bool(false),
		},
	), http.StatusOK, &distributedResp)
	var countPendingInstalls uint
	mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
		return sqlx.GetContext(ctx, q, &countPendingInstalls, `SELECT COUNT(*)
			FROM upcoming_activities
			WHERE activity_type = 'vpp_app_install'
			AND host_id = ?`, mdmHost.ID)
	})
	require.Equal(t, uint(1), countPendingInstalls)

	// send an idle request to grab the command uuid
	s.runWorker()
	var cmdUUID string
	cmd, err := mdmDevice.Idle()
	require.NoError(t, err)
	var fullCmd micromdm.CommandPayload
	require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd))
	cmdUUID = cmd.CommandUUID

	// Get pending activity, confirm one pending activity
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/activities/upcoming", mdmHost.ID),
		nil, http.StatusOK, &hostActivitiesResp)
	require.Len(t, hostActivitiesResp.Activities, 1, "got activities: %v", activitiesToString(hostActivitiesResp.Activities))
	assert.Equal(t, hostActivitiesResp.Activities[0].Type, fleet.ActivityInstalledAppStoreApp{}.ActivityName())
	assert.EqualValues(t, 1, hostActivitiesResp.Count)
	assert.JSONEq(
		t,
		fmt.Sprintf(
			`{"host_id": %d, "host_display_name": "%s", "software_title": "%s", "app_store_id": "%s", "command_uuid": "%s", "status": "%s", "self_service": %v}`,
			mdmHost.ID,
			mdmHost.DisplayName(),
			macOSApp.Name,
			macOSApp.AdamID,
			cmdUUID,
			fleet.SoftwareInstallPending,
			false,
		),
		string(*hostActivitiesResp.Activities[0].Details),
	)

	_, err = mdmDevice.Acknowledge(cmd.CommandUUID)
	require.NoError(t, err)

	s.runWorker()

	cmd, err = mdmDevice.Idle()
	require.NoError(t, err)
	for cmd != nil {
		var fullCmd micromdm.CommandPayload
		switch cmd.Command.RequestType { //nolint:gocritic // ignore singleCaseSwitch
		case "InstalledApplicationList":
			require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd))
			cmd, err = mdmDevice.AcknowledgeInstalledApplicationList(mdmDevice.UUID, cmd.CommandUUID, []fleet.Software{{Name: macOSApp.Name, BundleIdentifier: macOSApp.BundleIdentifier, Version: macOSApp.LatestVersion, Installed: true}})
			require.NoError(t, err)
		default:
			require.Fail(t, "unexpected command type", cmd.Command.RequestType)
		}
	}

	s.runWorker()

	// Shouldn't be any more pending installs
	mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
		return sqlx.GetContext(ctx, q, &countPendingInstalls, `SELECT COUNT(*)
			FROM upcoming_activities
			WHERE activity_type = 'vpp_app_install'
			AND host_id = ?`, mdmHost.ID)
	})
	require.Zero(t, countPendingInstalls)

	s.lastActivityMatchesExtended(
		fleet.ActivityInstalledAppStoreApp{}.ActivityName(),
		fmt.Sprintf(
			`{"host_id": %d, "host_display_name": "%s", "software_title": "%s", "app_store_id": "%s", "command_uuid": "%s", "status": "%s", "self_service": %v, "policy_id": %d, "policy_name": "%s"}`,
			mdmHost.ID,
			mdmHost.DisplayName(),
			macOSApp.Name,
			macOSApp.AdamID,
			cmdUUID,
			fleet.SoftwareInstalled,
			false,
			policy1Team1.ID,
			policy1Team1.Name,
		),
		0,
		ptr.Bool(true),
	)

	// Process script execution
	s.DoJSON("POST", "/api/fleet/orbit/config",
		json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *mdmHost2.OrbitNodeKey)),
		http.StatusOK, &orbitResp)
	require.Len(t, orbitResp.Notifications.PendingScriptExecutionIDs, 1)
	scriptExecID = orbitResp.Notifications.PendingScriptExecutionIDs[0]

	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/activities/upcoming", mdmHost2.ID),
		nil, http.StatusOK, &hostActivitiesResp)

	// app install + script exec
	require.Len(t, hostActivitiesResp.Activities, 2, "got activities: %v", activitiesToString(hostActivitiesResp.Activities))
	assert.Equal(t, hostActivitiesResp.Activities[0].Type, fleet.ActivityTypeRanScript{}.ActivityName())
	assert.EqualValues(t, 2, hostActivitiesResp.Count)
	assert.JSONEq(
		t,
		fmt.Sprintf(
			`{"host_id": %d, "host_display_name": "%s", "script_name": "%s", "async": true, "policy_id": %d, "policy_name": "%s", "script_execution_id": "%s", "batch_execution_id": null}`,
			mdmHost2.ID,
			mdmHost2.DisplayName(),
			savedTmScript.Name,
			policy3Team1.ID,
			policy3Team1.Name,
			scriptExecID,
		),
		string(*hostActivitiesResp.Activities[0].Details),
	)

	// setting a script result will activate the vpp install
	s.DoJSON("POST", "/api/fleet/orbit/scripts/result",
		json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q, "execution_id": %q, "exit_code": 0, "output": "ok"}`, *mdmHost2.OrbitNodeKey, scriptExecID)),
		http.StatusOK, &orbitPostScriptResp)

	s.lastActivityMatchesExtended(
		fleet.ActivityTypeRanScript{}.ActivityName(),
		fmt.Sprintf(
			`{"host_id": %d, "host_display_name": %q, "script_name": %q, "script_execution_id": %q, "async": true, "policy_id": %d, "policy_name": "%s", "batch_execution_id": null}`,
			mdmHost2.ID, mdmHost2.DisplayName(), savedTmScript.Name, scriptExecID, policy3Team1.ID, policy3Team1.Name,
		),
		0,
		ptr.Bool(true),
	)

	// Process mdmHost2's vpp installation
	s.runWorker()
	cmd, err = mdmDevice2.Idle()
	require.NoError(t, err)
	require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd))
	cmdUUID = cmd.CommandUUID

	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/activities/upcoming", mdmHost2.ID),
		nil, http.StatusOK, &hostActivitiesResp)

	require.Len(t, hostActivitiesResp.Activities, 1, "got activities: %v", activitiesToString(hostActivitiesResp.Activities))
	assert.Equal(t, hostActivitiesResp.Activities[0].Type, fleet.ActivityInstalledAppStoreApp{}.ActivityName())
	assert.EqualValues(t, 1, hostActivitiesResp.Count)
	assert.JSONEq(
		t,
		fmt.Sprintf(
			`{"host_id": %d, "host_display_name": "%s", "software_title": "%s", "app_store_id": "%s", "command_uuid": "%s", "status": "%s", "self_service": %v}`,
			mdmHost2.ID,
			mdmHost2.DisplayName(),
			macOSApp.Name,
			macOSApp.AdamID,
			cmdUUID,
			fleet.SoftwareInstallPending,
			false,
		),
		string(*hostActivitiesResp.Activities[0].Details),
	)

	_, err = mdmDevice2.Acknowledge(cmd.CommandUUID)
	require.NoError(t, err)

	s.runWorker()

	cmd, err = mdmDevice2.Idle()
	require.NoError(t, err)
	for cmd != nil {
		var fullCmd micromdm.CommandPayload
		switch cmd.Command.RequestType { //nolint:gocritic // ignore singleCaseSwitch
		case "InstalledApplicationList":
			require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd))
			cmd, err = mdmDevice2.AcknowledgeInstalledApplicationList(mdmDevice2.UUID, cmd.CommandUUID, []fleet.Software{{Name: macOSApp.Name, BundleIdentifier: macOSApp.BundleIdentifier, Version: macOSApp.LatestVersion, Installed: true}})
			require.NoError(t, err)
		default:
			require.Fail(t, "unexpected command type", cmd.Command.RequestType)
		}
	}

	mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
		return sqlx.GetContext(ctx, q, &countPendingInstalls, `SELECT COUNT(*)
			FROM upcoming_activities
			WHERE activity_type = 'vpp_app_install'
			AND host_id = ?`, mdmHost2.ID)
	})
	require.Zero(t, countPendingInstalls)

	s.lastActivityMatchesExtended(
		fleet.ActivityInstalledAppStoreApp{}.ActivityName(),
		fmt.Sprintf(
			`{"host_id": %d, "host_display_name": "%s", "software_title": "%s", "app_store_id": "%s", "command_uuid": "%s", "status": "%s", "self_service": %v, "policy_id": %d, "policy_name": "%s"}`,
			mdmHost2.ID,
			mdmHost2.DisplayName(),
			macOSApp.Name,
			macOSApp.AdamID,
			cmdUUID,
			fleet.SoftwareInstalled,
			false,
			policy3Team1.ID,
			policy3Team1.Name,
		),
		0,
		ptr.Bool(true),
	)

	// MDM host failing already-failing policies should not trigger any installs
	s.DoJSONWithoutAuth("POST", "/api/osquery/distributed/write", genDistributedReqWithPolicyResults(
		mdmHost,
		map[uint]*bool{
			policy1Team1.ID: ptr.Bool(false),
			policy2Team1.ID: ptr.Bool(false),
		},
	), http.StatusOK, &distributedResp)
	s.DoJSON("GET", "/api/latest/fleet/hosts/count", nil, http.StatusOK, &countResp, "software_status", "pending", "team_id",
		fmt.Sprint(team.ID), "software_title_id", fmt.Sprint(macOSTitleID))
	require.Equal(t, 0, countResp.Count)
}

func (s *integrationMDMTestSuite) TestEnrollmentProfilesWithSpecialChars() {
	t := s.T()
	ctx := context.Background()

	initialConfig, err := s.ds.AppConfig(ctx)
	require.NoError(t, err)
	initialSecrets, err := s.ds.GetEnrollSecrets(ctx, nil)
	require.NoError(t, err)

	nameWithInvalidChars := "Fleet & Device <3 Management"
	/* #nosec G101 -- this is a made up value for tests */
	enrollSecretWithInvalidChars := "1<2>3&4&/"

	acResp := appConfigResponse{}
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(fmt.Sprintf(`{
		"org_info": {
			"org_name": %q
		}
	}`, nameWithInvalidChars)), http.StatusOK, &acResp)
	enrollSecretResp := applyEnrollSecretSpecResponse{}
	s.DoJSON("POST", "/api/latest/fleet/spec/enroll_secret", applyEnrollSecretSpecRequest{
		Spec: &fleet.EnrollSecretSpec{
			Secrets: []*fleet.EnrollSecret{{Secret: enrollSecretWithInvalidChars}},
		},
	}, http.StatusOK, &enrollSecretResp)
	t.Cleanup(func() {
		acResp := appConfigResponse{}
		s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(fmt.Sprintf(`{
		  "org_info": {
			  "org_name": %q
		  }
		}`, initialConfig.OrgInfo.OrgName)), http.StatusOK, &acResp)
		s.DoJSON("POST", "/api/latest/fleet/spec/enroll_secret", applyEnrollSecretSpecRequest{
			Spec: &fleet.EnrollSecretSpec{
				Secrets: initialSecrets,
			},
		}, http.StatusOK, &enrollSecretResp)
	})

	// manual enrollment from My Device
	token := "token_test_manual_enroll"
	createHostAndDeviceToken(t, s.ds, token)
	var r getDeviceMDMManualEnrollProfileResponse
	s.DoJSON("GET", "/api/latest/fleet/device/"+token+"/mdm/apple/manual_enrollment_profile", nil, http.StatusOK, &r)
	u, err := url.Parse(r.EnrollURL)
	require.NoError(t, err)
	q, err := url.ParseQuery(u.RawQuery)
	require.NoError(t, err)
	require.Equal(t, enrollSecretWithInvalidChars, q.Get("enroll_secret"))
	s.downloadAndVerifyOTAEnrollmentProfile("/api/latest/fleet/enrollment_profiles/ota?enroll_secret=" + url.QueryEscape(q.Get("enroll_secret")))

	// automatic enrollment by token
	rawMsg := json.RawMessage(`{"allow_pairing": true}`)
	_, err = s.ds.NewMDMAppleEnrollmentProfile(ctx, fleet.MDMAppleEnrollmentProfilePayload{
		Type:       "automatic",
		DEPProfile: &rawMsg,
		Token:      "abcd",
	})
	require.NoError(t, err)

	di, err := mdmtest.EncodeDeviceInfo(fleet.MDMAppleMachineInfo{
		Serial: uuid.New().String(),
		UDID:   uuid.New().String(),
	})
	require.NoError(t, err)
	s.downloadAndVerifyEnrollmentProfile(t, optsDownloadEnrollProf{
		basePath: apple_mdm.EnrollPath,
		token:    "abcd",
		diParam:  di,
	})

	// unsigned manual enrollment profile for IT admins
	s.downloadAndVerifyEnrollmentProfileManual(t)

	// ensure the fleetd profile sends a good enroll secret too
	s.awaitTriggerProfileSchedule(t)
	prof := s.assertConfigProfilesByIdentifier(nil, mobileconfig.FleetdConfigPayloadIdentifier, true)

	type fleetdPlist struct {
		PayloadContent []struct {
			EnrollSecret string `plist:"EnrollSecret"`
		} `plist:"PayloadContent"`
	}

	// Parse the plist data
	var parsedData fleetdPlist
	err = plist.NewDecoder(bytes.NewReader(prof.Mobileconfig)).Decode(&parsedData)
	require.NoError(t, err)
	require.Equal(t, enrollSecretWithInvalidChars, parsedData.PayloadContent[0].EnrollSecret)
}

func (s *integrationMDMTestSuite) TestOTAEnrollment() {
	t := s.T()

	// create a global enroll secret
	globalSecret := "global_secret"
	var applyResp applyEnrollSecretSpecResponse
	s.DoJSON("POST", "/api/latest/fleet/spec/enroll_secret", applyEnrollSecretSpecRequest{
		Spec: &fleet.EnrollSecretSpec{
			Secrets: []*fleet.EnrollSecret{{Secret: globalSecret}},
		},
	}, http.StatusOK, &applyResp)

	reqBody := []byte(`<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>PRODUCT</key>
	<string></string>
	<key>SERIAL</key>
	<string>foo</string>
	<key>UDID</key>
	<string></string>
	<key>VERSION</key>
	<string></string>
</dict>
</plist>`)

	// Setup signed req body, but also verify we can sign it.
	cert, key, err := apple_mdm.NewSCEPCACertKey()
	require.NoError(t, err)
	signedData, err := pkcs7.NewSignedData(reqBody)
	require.NoError(t, err)
	require.NoError(t, signedData.AddSigner(cert, key, pkcs7.SignerInfoConfig{}))
	signedReqBody, err := signedData.Finish()
	require.NoError(t, err)

	t.Run("errors", func(t *testing.T) {
		t.Run("if no enroll secret is provided", func(t *testing.T) {
			httpResp := s.DoRawNoAuth("POST", "/api/latest/fleet/ota_enrollment", reqBody, http.StatusForbidden)
			errMsg := extractServerErrorText(httpResp.Body)
			require.Contains(t, errMsg, "Couldn't install the profile. Invalid enroll secret. Please contact your IT admin.")
			require.NoError(t, httpResp.Body.Close())
		})

		t.Run("if no body is provided", func(t *testing.T) {
			httpResp := s.DoRawNoAuth("POST", "/api/latest/fleet/ota_enrollment?enroll_secret=foo", nil, http.StatusBadRequest)
			errMsg := extractServerErrorText(httpResp.Body)
			require.Contains(t, errMsg, "invalid request body")
			require.NoError(t, httpResp.Body.Close())
		})

		t.Run("if body is unsigned", func(t *testing.T) {
			httpResp := s.DoRawNoAuth("POST", "/api/latest/fleet/ota_enrollment?enroll_secret=foo", reqBody, http.StatusBadRequest)
			errMsg := extractServerErrorText(httpResp.Body)
			require.Contains(t, errMsg, "invalid request body")
			require.NoError(t, httpResp.Body.Close())
		})

		t.Run("if invalid apple signature", func(t *testing.T) {
			httpResp := s.DoRawNoAuth("POST", "/api/latest/fleet/ota_enrollment?enroll_secret=foo", signedReqBody, http.StatusForbidden)
			errMsg := extractServerErrorText(httpResp.Body)
			require.Contains(t, errMsg, "Couldn't install the profile. Invalid enroll secret. Please contact your IT admin.")
			require.NoError(t, httpResp.Body.Close())
		})

		t.Run("if invalid device signature", func(t *testing.T) {
			t.Setenv("FLEET_DEV_MDM_APPLE_DISABLE_DEVICE_INFO_CERT_VERIFY", "1")
			httpResp := s.DoRawNoAuth("POST", "/api/latest/fleet/ota_enrollment?enroll_secret=foo", signedReqBody, http.StatusForbidden)
			errMsg := extractServerErrorText(httpResp.Body)
			require.Contains(t, errMsg, "Couldn't install the profile. Invalid enroll secret. Please contact your IT admin.")
			require.NoError(t, httpResp.Body.Close())
		})

		t.Run("if serial is missing", func(t *testing.T) {
			signedData, err = pkcs7.NewSignedData([]byte(`<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>SERIAL</key>
	<string></string>
</dict>
</plist>`))
			require.NoError(t, err)
			require.NoError(t, signedData.AddSigner(cert, key, pkcs7.SignerInfoConfig{}))
			signedReqBody, err = signedData.Finish()
			require.NoError(t, err)
			httpResp := s.DoRawNoAuth("POST", "/api/latest/fleet/ota_enrollment?enroll_secret=foo", signedReqBody, http.StatusBadRequest)
			errMsg := extractServerErrorText(httpResp.Body)
			require.Contains(t, errMsg, "SERIAL is required")
			require.NoError(t, httpResp.Body.Close())
		})

		t.Run("if idp uuid does not match an account", func(t *testing.T) {
			hwModel := "MacBookPro16,1"
			mdmDevice := mdmtest.NewTestMDMClientAppleOTA(
				s.server.URL,
				globalSecret,
				hwModel,
				mdmtest.WithOTAIdpUUID(uuid.New().String()),
			)
			err := mdmDevice.Enroll()
			require.Error(t, err)
			require.Contains(t, err.Error(), "404 Not Found")
		})

		t.Run("if idp uuid is required but not set", func(t *testing.T) {
			var specResp applyTeamSpecsResponse
			teamSecret := "team_secret"
			teamSpecs := applyTeamSpecsRequest{Specs: []*fleet.TeamSpec{{Name: "newteam", Secrets: &[]fleet.EnrollSecret{{Secret: teamSecret}}, MDM: fleet.TeamSpecMDM{MacOSSetup: fleet.MacOSSetup{EnableEndUserAuthentication: true}}}}}
			s.DoJSON("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK, &specResp)

			hwModel := "MacBookPro16,1"
			mdmDevice := mdmtest.NewTestMDMClientAppleOTA(
				s.server.URL,
				"team_secret",
				hwModel,
			)
			err := mdmDevice.Enroll()
			require.Error(t, err)
			require.Contains(t, err.Error(), "403 Forbidden")
		})
	})

	t.Run("succeeds", func(t *testing.T) {
		verifySuccessfulOTAEnrollment := func(mdmDevice *mdmtest.TestAppleMDMClient, hwModel, platform string, enrollTime time.Time) getHostResponse {
			var hostByIdentifierResp getHostResponse
			s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/identifier/%s", mdmDevice.UUID), nil, http.StatusOK, &hostByIdentifierResp)
			require.Equal(t, hwModel, hostByIdentifierResp.Host.HardwareModel)
			require.Equal(t, platform, hostByIdentifierResp.Host.Platform)
			require.NotNil(t, hostByIdentifierResp.Host.LastMDMEnrolledAt)
			assert.GreaterOrEqual(t, *hostByIdentifierResp.Host.LastMDMEnrolledAt, enrollTime)
			require.NotNil(t, hostByIdentifierResp.Host.LastMDMCheckedInAt)
			assert.GreaterOrEqual(t, *hostByIdentifierResp.Host.LastMDMCheckedInAt, enrollTime)

			return hostByIdentifierResp
		}

		verifySuccessfulIdpAssociation := func(hostUUID, idpUUID string) {
			hostToIdpMap, err := s.ds.GetMDMIdPAccountsByHostUUIDs(context.Background(), []string{hostUUID})
			require.NoError(t, err)
			idpAccount := hostToIdpMap[hostUUID]
			require.NotNil(t, idpAccount)
			require.Equal(t, idpUUID, idpAccount.UUID)
		}

		t.Run("ota enrolling a macbook", func(t *testing.T) {
			hwModel := "MacBookPro16,1"
			mdmDevice := mdmtest.NewTestMDMClientAppleOTA(
				s.server.URL,
				globalSecret,
				hwModel,
			)
			enrollTime := time.Now().UTC().Truncate(time.Second)
			require.NoError(t, mdmDevice.Enroll())
			s.runWorker()
			checkInstallFleetdCommandSent(t, mdmDevice, true)

			hostByIdentifierResp := verifySuccessfulOTAEnrollment(mdmDevice, hwModel, "darwin", enrollTime)
			require.Nil(t, hostByIdentifierResp.Host.TeamID)
		})

		t.Run("ota enrolling an ipad", func(t *testing.T) {
			var specResp applyTeamSpecsResponse
			teamSecret := "team_secret"
			teamSpecs := applyTeamSpecsRequest{Specs: []*fleet.TeamSpec{{Name: "newteam", Secrets: &[]fleet.EnrollSecret{{Secret: teamSecret}}}}}
			s.DoJSON("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK, &specResp)

			hwModel := "iPad13,16"
			mdmDevice := mdmtest.NewTestMDMClientAppleOTA(
				s.server.URL,
				teamSecret,
				hwModel,
			)
			enrollTime := time.Now().UTC().Truncate(time.Second)
			require.NoError(t, mdmDevice.Enroll())
			s.runWorker()
			checkInstallFleetdCommandSent(t, mdmDevice, false)

			hostByIdentifierResp := verifySuccessfulOTAEnrollment(mdmDevice, hwModel, "ipados", enrollTime)
			require.NotNil(t, hostByIdentifierResp.Host.TeamID)
			require.Equal(t, specResp.TeamIDsByName["newteam"], *hostByIdentifierResp.Host.TeamID)
		})

		t.Run("ota enrollment if idp_uuid is not set does not associate host with idp account", func(t *testing.T) {
			hwModel := "MacBookPro16,1"
			mdmDevice := mdmtest.NewTestMDMClientAppleOTA(
				s.server.URL,
				globalSecret,
				hwModel,
			)
			enrollTime := time.Now().UTC().Truncate(time.Second)
			require.NoError(t, mdmDevice.Enroll())
			s.runWorker()
			checkInstallFleetdCommandSent(t, mdmDevice, true)

			resp := verifySuccessfulOTAEnrollment(mdmDevice, hwModel, "darwin", enrollTime)
			account, err := s.ds.GetMDMIdPAccountByHostUUID(context.Background(), resp.Host.UUID)
			require.NoError(t, err)
			require.Nil(t, account) // We do not fail but return nil for both if not found, and we do not expect an entry here.
		})

		t.Run("ota enrollment if idp_uuid is set associates host with idp account", func(t *testing.T) {
			idpEmail := "test@example.com"
			err := s.ds.InsertMDMIdPAccount(context.Background(), &fleet.MDMIdPAccount{
				Username: "test",
				Email:    idpEmail,
			})
			require.NoError(t, err)
			idpAccount, err := s.ds.GetMDMIdPAccountByEmail(context.Background(), idpEmail)
			require.NoError(t, err)

			hwModel := "MacBookPro16,1"
			mdmDevice := mdmtest.NewTestMDMClientAppleOTA(
				s.server.URL,
				globalSecret,
				hwModel,
				mdmtest.WithOTAIdpUUID(idpAccount.UUID),
			)
			enrollTime := time.Now().UTC().Truncate(time.Second)
			require.NoError(t, mdmDevice.Enroll())
			s.runWorker()
			checkInstallFleetdCommandSent(t, mdmDevice, true)

			resp := verifySuccessfulOTAEnrollment(mdmDevice, hwModel, "darwin", enrollTime)
			verifySuccessfulIdpAssociation(resp.Host.UUID, idpAccount.UUID)
		})
	})
}

func (s *integrationMDMTestSuite) TestAppleMDMAccountDrivenUserEnrollment() {
	t := s.T()

	// We are going to login and do some SSO operations then do the actual enrollments during the
	// test. Why? Testing the SAML SSO flow requires specific hardcoded server values but the actual
	// server URL changes between runs, so once we've done the SSO operations we reset the server config
	// to the proper value and then fetch the enrollment profiles and do the enrollments.
	// TODO: Is there a better way to do this?
	originalServerUrl := s.server.URL
	s.setUpMDMSSO(t)

	getSSOAccessToken := func(username, password string) string {
		ssoResult := s.LoginAccountDrivenEnrollUser(username, password)
		// check location for apple-remotemanagement-user-login://authentication-results?access-token=
		returnedLocation, err := ssoResult.Location()
		require.NoError(t, err)
		require.NotNil(t, returnedLocation)
		returnedLocationURL := returnedLocation.String()
		require.True(t, strings.HasPrefix(returnedLocationURL, "apple-remotemanagement-user-login://authentication-results?access-token="))
		accessToken := strings.Split(returnedLocationURL, "apple-remotemanagement-user-login://authentication-results?access-token=")[1]
		return accessToken
	}

	iPhoneAccessToken := getSSOAccessToken("sso_user", "user123#")
	iPadAccessToken := getSSOAccessToken("sso_user2", "user123#")

	acResp := appConfigResponse{}
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
		"server_settings": {
			"server_url": "`+originalServerUrl+`"
		}
	}`), http.StatusOK, &acResp)

	iPhoneHwModel := "iPhone14,5"
	iPhoneMdmDevice := mdmtest.NewTestMDMClientAppleAccountDrivenUserEnrollment(
		s.server.URL,
		iPhoneHwModel,
		"", // No bearer token
	)
	require.ErrorContains(t, iPhoneMdmDevice.Enroll(), "401 Unauthorized")

	iPhoneMdmDevice = mdmtest.NewTestMDMClientAppleAccountDrivenUserEnrollment(
		s.server.URL,
		iPhoneHwModel,
		iPhoneAccessToken,
	)
	require.NoError(t, iPhoneMdmDevice.Enroll())
	assert.Equal(t, iPhoneMdmDevice.EnrollInfo.AssignedManagedAppleID, "sso_user@example.com")

	s.lastActivityOfTypeMatches(fleet.ActivityTypeMDMEnrolled{}.ActivityName(),
		fmt.Sprintf(`{"host_serial": null, "enrollment_id": "%s", "host_display_name": "%s (%s)", "installed_from_dep": false, "mdm_platform": "apple", "platform": "ios"}`, iPhoneMdmDevice.EnrollmentID(), iPhoneMdmDevice.Model, iPhoneMdmDevice.EnrollmentID()), 0)
	linkedIDPAccount, err := s.ds.GetMDMIdPAccountByHostUUID(context.Background(), iPhoneMdmDevice.EnrollmentID())
	require.NoError(t, err)
	require.NotNil(t, linkedIDPAccount)
	assert.Equal(t, linkedIDPAccount.Email, "sso_user@example.com")

	iPadHwModel := "iPad14,5"
	iPadMdmDevice := mdmtest.NewTestMDMClientAppleAccountDrivenUserEnrollment(
		s.server.URL,
		iPadHwModel,
		"", // No bearer token
	)
	require.ErrorContains(t, iPadMdmDevice.Enroll(), "401 Unauthorized")

	iPadMdmDevice = mdmtest.NewTestMDMClientAppleAccountDrivenUserEnrollment(
		s.server.URL,
		iPadHwModel,
		iPadAccessToken,
	)
	require.NoError(t, iPadMdmDevice.Enroll())
	assert.Equal(t, iPadMdmDevice.EnrollInfo.AssignedManagedAppleID, "sso_user2@example.com")

	s.lastActivityOfTypeMatches(fleet.ActivityTypeMDMEnrolled{}.ActivityName(),
		fmt.Sprintf(`{"host_serial": null, "enrollment_id": "%s", "host_display_name": "%s (%s)", "installed_from_dep": false, "mdm_platform": "apple", "platform": "ipados"}`, iPadMdmDevice.EnrollmentID(), iPadMdmDevice.Model, iPadMdmDevice.EnrollmentID()), 0)
	linkedIDPAccount, err = s.ds.GetMDMIdPAccountByHostUUID(context.Background(), iPadMdmDevice.EnrollmentID())
	require.NoError(t, err)
	require.NotNil(t, linkedIDPAccount)
	assert.Equal(t, linkedIDPAccount.Email, "sso_user2@example.com")

	// Account driven enrollment is not yet supported for macOS devices so we expect an error in
	// either case
	macbookHwModel := "Mac16,1"
	macbookMdmDevice := mdmtest.NewTestMDMClientAppleAccountDrivenUserEnrollment(
		s.server.URL,
		macbookHwModel,
		"", // No bearer token
	)

	// A different error than usual for no bearer token because the system will try
	// to initiate the SSO flow if the server returns a 401 unauthorized, and we don't
	// want that.
	require.ErrorContains(t, macbookMdmDevice.Enroll(), "400 Bad Request")

	macbookMdmDevice = mdmtest.NewTestMDMClientAppleAccountDrivenUserEnrollment(
		s.server.URL,
		macbookHwModel,
		iPadAccessToken, // Using iPad's access token. It doesn't matter because it should fail before checking it
	)
	require.ErrorContains(t, macbookMdmDevice.Enroll(), "400 Bad Request")

	// Fetch the hosts we enrolled and check their details. Specifically we want to verify that MDM
	// shows a personal enrollment via all 3 endpoints(list hosts, host details and host MDM
	// details) since they query for the information slightly differently
	listHostsRes := listHostsResponse{}
	var iPhoneHostID *uint
	var iPadHostID *uint
	s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listHostsRes)
	require.Len(t, listHostsRes.Hosts, 2)
	for _, host := range listHostsRes.Hosts {
		if host.UUID == iPhoneMdmDevice.EnrollmentID() {
			assert.Equal(t, iPhoneHwModel, host.HardwareModel)
			assert.Equal(t, iPhoneMdmDevice.EnrollmentID(), host.UUID)
			assert.Equal(t, iPhoneMdmDevice.EnrollmentID(), host.HardwareSerial)
			require.NotNil(t, host.MDM.EnrollmentStatus)
			assert.Equal(t, "On (personal)", *host.MDM.EnrollmentStatus)
			assert.True(t, *host.MDM.ConnectedToFleet)
			iPhoneHostID = ptr.Uint(host.ID)
		} else if host.UUID == iPadMdmDevice.EnrollmentID() {
			assert.Equal(t, "ipados", host.Platform)
			assert.Equal(t, iPadMdmDevice.EnrollmentID(), host.UUID)
			assert.Equal(t, iPadMdmDevice.EnrollmentID(), host.HardwareSerial)
			assert.Equal(t, iPadHwModel, host.HardwareModel)
			require.NotNil(t, host.MDM.EnrollmentStatus)
			assert.Equal(t, "On (personal)", *host.MDM.EnrollmentStatus)
			assert.True(t, *host.MDM.ConnectedToFleet)
			iPadHostID = ptr.Uint(host.ID)
		}
	}

	require.NotNil(t, iPhoneHostID, "iPhone host not found in the list of hosts")
	require.NotNil(t, iPadHostID, "iPad host not found in the list of hosts")

	// Confirm that host details endpoint contains the expected values for the iPhone
	getHostResp := getDeviceHostResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", *iPhoneHostID), nil, http.StatusOK, &getHostResp)
	require.NotNil(t, getHostResp.Host.MDM.EnrollmentStatus)
	assert.Equal(t, "On (personal)", *getHostResp.Host.MDM.EnrollmentStatus)
	assert.True(t, *getHostResp.Host.MDM.ConnectedToFleet)
	assert.Equal(t, iPhoneHwModel, getHostResp.Host.HardwareModel)

	// Confirm that the host MDM endpoint contains the expected values for the iPhone
	// using an inline struct because the definition of fleet.HostMDM makes it unusable here
	getHostMDMResponse := struct {
		EnrollmentStatus string `json:"enrollment_status"`
		ServerURL        string `json:"server_url"`
		Name             string `json:"name"`
		ID               uint   `json:"id"`
	}{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/mdm", *iPhoneHostID), nil, http.StatusOK, &getHostMDMResponse)
	assert.Equal(t, "On (personal)", getHostMDMResponse.EnrollmentStatus)
	assert.Equal(t, fleet.WellKnownMDMFleet, getHostMDMResponse.Name)

	// Confirm that host details endpoint contains the expected values for the iPad
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", *iPadHostID), nil, http.StatusOK, &getHostResp)
	require.NotNil(t, getHostResp.Host.MDM.EnrollmentStatus)
	assert.Equal(t, "On (personal)", *getHostResp.Host.MDM.EnrollmentStatus)
	assert.True(t, *getHostResp.Host.MDM.ConnectedToFleet)

	// Confirm that the host MDM endpoint contains the expected values for the iPad
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/mdm", *iPadHostID), nil, http.StatusOK, &getHostMDMResponse)
	assert.Equal(t, "On (personal)", getHostMDMResponse.EnrollmentStatus)
	assert.Equal(t, fleet.WellKnownMDMFleet, getHostMDMResponse.Name)
}

func (s *integrationMDMTestSuite) TestAppleMDMActionsOnPersonalHost() {
	t := s.T()

	// We are going to login and do some SSO operations then do the actual enrollments during the
	// test. Why? Testing the SAML SSO flow requires specific hardcoded server values but the actual
	// server URL changes between runs, so once we've done the SSO operations we reset the server config
	// to the proper value and then fetch the enrollment profiles and do the enrollments.
	// TODO: Is there a better way to do this?
	originalServerUrl := s.server.URL
	s.setUpMDMSSO(t)

	getSSOAccessToken := func(username, password string) string {
		ssoResult := s.LoginAccountDrivenEnrollUser(username, password)
		// check location for apple-remotemanagement-user-login://authentication-results?access-token=
		returnedLocation, err := ssoResult.Location()
		require.NoError(t, err)
		require.NotNil(t, returnedLocation)
		returnedLocationURL := returnedLocation.String()
		require.True(t, strings.HasPrefix(returnedLocationURL, "apple-remotemanagement-user-login://authentication-results?access-token="))
		accessToken := strings.Split(returnedLocationURL, "apple-remotemanagement-user-login://authentication-results?access-token=")[1]
		return accessToken
	}

	iPhoneAccessToken := getSSOAccessToken("sso_user", "user123#")

	acResp := appConfigResponse{}
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
		"server_settings": {
			"server_url": "`+originalServerUrl+`"
		}
	}`), http.StatusOK, &acResp)

	iPhoneHwModel := "iPhone14,5"

	iPhoneMdmDevice := mdmtest.NewTestMDMClientAppleAccountDrivenUserEnrollment(
		s.server.URL,
		iPhoneHwModel,
		iPhoneAccessToken,
	)
	require.NoError(t, iPhoneMdmDevice.Enroll())
	assert.Equal(t, iPhoneMdmDevice.EnrollInfo.AssignedManagedAppleID, "sso_user@example.com")

	listHostsRes := listHostsResponse{}
	s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listHostsRes)
	require.Len(t, listHostsRes.Hosts, 1)
	host := listHostsRes.Hosts[0]
	assert.Equal(t, host.UUID, iPhoneMdmDevice.EnrollmentID())
	require.NotNil(t, host.MDM.EnrollmentStatus)
	assert.Equal(t, "On (personal)", *host.MDM.EnrollmentStatus)
	assert.True(t, *host.MDM.ConnectedToFleet)

	// Confirm that locking or wiping the host fails with an appropriate error
	// Lock the host
	r := s.DoRaw("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/lock", host.ID), nil, http.StatusBadRequest)
	require.Contains(t, extractServerErrorText(r.Body), fleet.CantLockPersonalHostsMessage)

	// try to wipe the host
	r = s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/wipe", host.ID), nil, http.StatusBadRequest)
	require.Contains(t, extractServerErrorText(r.Body), fleet.CantWipePersonalHostsMessage)

	// Confirm that turning off MDM for personal hosts are allowed - NEEDS to be last, to not turn off MDM for the other checks.
	s.Do("DELETE", fmt.Sprintf("/api/latest/fleet/hosts/%d/mdm", host.ID), nil, http.StatusNoContent)
}

func (s *integrationMDMTestSuite) TestSCEPProxy() {
	s.runSCEPProxyTestWithOptionalSuffix("")
}

func (s *integrationMDMTestSuite) TestSCEPProxyWithMicrosoftSuffix() {
	// Windows SCEP client appends pkiclient.exe to the URL for all requests
	s.runSCEPProxyTestWithOptionalSuffix("/pkiclient.exe")
}

// suffix is optional but will be tested with all endpoints. The idea of this is to ensure
// we get the exact same behavior whether it is used or not.
func (s *integrationMDMTestSuite) runSCEPProxyTestWithOptionalSuffix(suffix string) {
	t := s.T()
	ctx := context.Background()

	// Add an MDM profile
	globalProfiles := [][]byte{
		mobileconfigForTest("N1", "Good1"),
		mobileconfigForTest("N2", "Bad1"),
	}
	// add global profile
	s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: globalProfiles}, http.StatusNoContent)

	// Create a host and then enroll to MDM.
	host, mdmDevice := createHostThenEnrollMDM(s.ds, s.server.URL, t)
	setupPusher(s, t, mdmDevice)
	// trigger a profile sync
	s.awaitTriggerProfileSchedule(t)
	profiles, err := s.ds.GetHostMDMAppleProfiles(ctx, host.UUID)
	require.NoError(t, err)
	require.GreaterOrEqual(t, len(profiles), 2)
	var profileUUID string
	var goodProfile, badProfile fleet.HostMDMAppleProfile
	for _, profile := range profiles {
		switch profile.Identifier {
		case "Good1":
			profileUUID = profile.ProfileUUID
			goodProfile = profile
		case "Bad1":
			profile.Status = &fleet.MDMDeliveryFailed
			badProfile = profile
		}
	}
	err = s.ds.BulkUpsertMDMAppleHostProfiles(ctx, []*fleet.MDMAppleBulkUpsertHostProfilePayload{{
		ProfileUUID:       badProfile.ProfileUUID,
		ProfileIdentifier: badProfile.Identifier,
		ProfileName:       badProfile.Name,
		HostUUID:          host.UUID,
		CommandUUID:       badProfile.CommandUUID,
		OperationType:     badProfile.OperationType,
		Status:            badProfile.Status,
		Detail:            badProfile.Detail,
		Checksum:          []byte("checksum"),
		Scope:             fleet.PayloadScopeSystem,
	}, {
		ProfileUUID:       goodProfile.ProfileUUID,
		ProfileIdentifier: goodProfile.Identifier,
		ProfileName:       goodProfile.Name,
		HostUUID:          host.UUID,
		CommandUUID:       goodProfile.CommandUUID,
		OperationType:     goodProfile.OperationType,
		Status:            goodProfile.Status,
		Detail:            goodProfile.Detail,
		Checksum:          []byte("checksum"),
		Scope:             fleet.PayloadScopeSystem,
	}})
	require.NoError(t, err)
	err = s.ds.BulkUpsertMDMManagedCertificates(ctx, []*fleet.MDMManagedCertificate{
		{
			HostUUID:    host.UUID,
			ProfileUUID: profileUUID,
			Type:        fleet.CAConfigNDES,
			CAName:      "NDES",
		},
		{
			HostUUID:    host.UUID,
			ProfileUUID: badProfile.ProfileUUID,
			Type:        fleet.CAConfigNDES,
			CAName:      "NDES",
		},
	})
	require.NoError(t, err)

	identifier := url.PathEscape(host.UUID + "," + profileUUID)
	badIdentifier := url.PathEscape(host.UUID + "," + badProfile.ProfileUUID)

	data, err := os.ReadFile("./testdata/PKCSReq.der")
	require.NoError(t, err)
	message := base64.StdEncoding.EncodeToString(data)

	// NDES not configured
	res := s.DoRawNoAuth("GET", apple_mdm.SCEPProxyPath+identifier+suffix, nil, http.StatusBadRequest)
	errBody, err := io.ReadAll(res.Body)
	require.NoError(t, err)
	assert.Contains(t, string(errBody), "missing operation")
	// Provide SCEP operation (GetCACaps)
	res = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+identifier+suffix, nil, http.StatusBadRequest, nil, "operation", "GetCACaps")
	errBody, err = io.ReadAll(res.Body)
	require.NoError(t, err)
	assert.Contains(t, string(errBody), eeservice.MessageSCEPProxyNotConfigured)
	// Provide SCEP operation (GetCACerts)
	res = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+identifier+suffix, nil, http.StatusBadRequest, nil, "operation", "GetCACert")
	errBody, err = io.ReadAll(res.Body)
	require.NoError(t, err)
	assert.Contains(t, string(errBody), eeservice.MessageSCEPProxyNotConfigured)
	// Provide SCEP operation (PKIOperation)
	res = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+identifier+suffix, nil, http.StatusBadRequest, nil, "operation", "PKIOperation",
		"message", message)
	errBody, err = io.ReadAll(res.Body)
	require.NoError(t, err)
	assert.Contains(t, string(errBody), eeservice.MessageSCEPProxyNotConfigured)
	// Provide SCEP operation (GetNextCACert)
	res = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+identifier+suffix, nil, http.StatusBadRequest, nil, "operation", "GetNextCACert")
	errBody, err = io.ReadAll(res.Body)
	require.NoError(t, err)
	assert.Contains(t, string(errBody), "not implemented")

	// Configure a bad SCEP URL using local test server
	testServer, cleanup := createTestServerWithStatusCode(http.StatusGone)
	defer cleanup()

	// Bypass service validation to insert a bad url
	adminUrl := "bad-admin-url"
	username := "user"
	password := "pass"
	ca, err := s.ds.NewCertificateAuthority(ctx, &fleet.CertificateAuthority{
		Type:     string(fleet.CATypeNDESSCEPProxy),
		Name:     ptr.String("bad-url"),
		URL:      &testServer.URL,
		AdminURL: &adminUrl,
		Username: &username,
		Password: &password,
	})
	require.NoError(t, err)

	res = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+identifier+suffix, nil, http.StatusInternalServerError, nil, "operation", "GetCACaps")
	errBody, err = io.ReadAll(res.Body)
	require.NoError(t, err)
	assert.Contains(t, string(errBody), "Could not GetCACaps from SCEP server")
	res = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+identifier+suffix, nil, http.StatusInternalServerError, nil, "operation", "GetCACert")
	errBody, err = io.ReadAll(res.Body)
	require.NoError(t, err)
	assert.Contains(t, string(errBody), "Could not GetCACert from SCEP server")
	res = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+identifier+suffix, nil, http.StatusInternalServerError, nil, "operation",
		"PKIOperation", "message", message)
	errBody, err = io.ReadAll(res.Body)
	require.NoError(t, err)
	assert.Contains(t, string(errBody), "Could not do PKIOperation on SCEP server")

	// Test timeout error
	ndesTimeoutServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		time.Sleep(1 * time.Second)
		w.WriteHeader(http.StatusOK)
	}))
	*s.scepConfig.Timeout = time.Microsecond
	t.Cleanup(func() {
		*s.scepConfig.Timeout = 10 * time.Second
		ndesTimeoutServer.Close()
	})

	// Bypass service validation to verify timeout behaviour
	s.DoRaw("DELETE", fmt.Sprintf("/api/latest/fleet/certificate_authorities/%d", ca.ID), nil, http.StatusNoContent)
	ca, err = s.ds.NewCertificateAuthority(ctx, &fleet.CertificateAuthority{
		Type:     string(fleet.CATypeNDESSCEPProxy),
		Name:     ptr.String("bad-url"),
		URL:      &ndesTimeoutServer.URL,
		AdminURL: &adminUrl,
		Username: &username,
		Password: &password,
	})
	require.NoError(t, err)

	// GetCACaps
	_ = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+identifier+suffix, nil, http.StatusRequestTimeout, nil, "operation", "GetCACaps")
	// GetCACert
	_ = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+identifier+suffix, nil, http.StatusRequestTimeout, nil, "operation", "GetCACert")
	// PKIOperation
	_ = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+identifier+suffix, nil, http.StatusRequestTimeout, nil, "operation",
		"PKIOperation", "message", message)
	*s.scepConfig.Timeout = 10 * time.Second

	scepServer := scep_server.StartTestSCEPServer(t)

	s.DoRaw("DELETE", fmt.Sprintf("/api/latest/fleet/certificate_authorities/%d", ca.ID), nil, http.StatusNoContent)
	_, err = s.ds.NewCertificateAuthority(ctx, &fleet.CertificateAuthority{
		Type:     string(fleet.CATypeNDESSCEPProxy),
		Name:     ptr.String("bad-url"),
		URL:      ptr.String(scepServer.URL + "/scep"),
		AdminURL: &adminUrl,
		Username: &username,
		Password: &password,
	})
	require.NoError(t, err)

	// GetCACaps
	res = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+identifier+suffix, nil, http.StatusOK, nil, "operation", "GetCACaps")
	body, err := io.ReadAll(res.Body)
	require.NoError(t, err)
	assert.Equal(t, scepserver.DefaultCACaps, string(body))

	// GetCACert
	res = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+identifier+suffix, nil, http.StatusOK, nil, "operation", "GetCACert")
	body, err = io.ReadAll(res.Body)
	require.NoError(t, err)
	certs, err := x509.ParseCertificates(body)
	require.NoError(t, err)
	assert.Len(t, certs, 1)

	// PKIOperation
	// Invalid identifier format
	res = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+"bozo"+suffix, nil, http.StatusBadRequest, nil, "operation",
		"PKIOperation", "message", message)
	errBody, err = io.ReadAll(res.Body)
	require.NoError(t, err)
	assert.Contains(t, string(errBody), "invalid identifier")
	// Non-Apple config profile (missing leading 'a')
	res = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+"bozoHost%2CbozoProfile"+suffix, nil, http.StatusBadRequest, nil, "operation",
		"PKIOperation", "message", message)
	errBody, err = io.ReadAll(res.Body)
	require.NoError(t, err)
	assert.Contains(t, string(errBody), "invalid profile UUID")
	// Unknown host/profile
	res = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+"bozoHost%2CabozoProfile"+suffix, nil, http.StatusBadRequest, nil, "operation",
		"PKIOperation", "message", message)
	errBody, err = io.ReadAll(res.Body)
	require.NoError(t, err)
	assert.Contains(t, string(errBody), "unknown identifier")
	// Profile which is not pending
	res = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+badIdentifier+suffix, nil, http.StatusBadRequest, nil, "operation",
		"PKIOperation", "message", message)
	errBody, err = io.ReadAll(res.Body)
	require.NoError(t, err)
	assert.Contains(t, string(errBody), "profile status")

	// "Good" request without one-time challenge.
	// Note, a cert is not returned here because the message is not a fully valid SCEP request. However, building a valid SCEP request is a bit involved,
	// and this request is sufficient for testing our proxy functionality.
	res = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+identifier+suffix, nil, http.StatusOK, nil, "operation",
		"PKIOperation", "message", message)
	body, err = io.ReadAll(res.Body)
	require.NoError(t, err)
	pkiMessage, err := scep.ParsePKIMessage(body, scep.WithCACerts(certs))
	require.NoError(t, err)
	assert.Equal(t, scep.CertRep, pkiMessage.MessageType)

	// Expired challenge
	err = s.ds.BulkUpsertMDMManagedCertificates(ctx, []*fleet.MDMManagedCertificate{
		{
			HostUUID:             host.UUID,
			ProfileUUID:          profileUUID,
			ChallengeRetrievedAt: ptr.Time(time.Now().Add(-eeservice.NDESChallengeInvalidAfter)),
			Type:                 fleet.CAConfigNDES,
			CAName:               "NDES",
		},
	})
	require.NoError(t, err)
	res = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+identifier+suffix, nil, http.StatusBadRequest, nil, "operation",
		"PKIOperation", "message", message)
	errBody, err = io.ReadAll(res.Body)
	require.NoError(t, err)
	assert.Contains(t, string(errBody), "challenge password")

	// Non-expired challenge
	err = s.ds.BulkUpsertMDMManagedCertificates(ctx, []*fleet.MDMManagedCertificate{
		{
			HostUUID:             host.UUID,
			ProfileUUID:          profileUUID,
			ChallengeRetrievedAt: ptr.Time(time.Now().Add(-eeservice.NDESChallengeInvalidAfter + time.Minute)),
			Type:                 fleet.CAConfigNDES,
			CAName:               "NDES",
		},
	})
	require.NoError(t, err)

	// Profile status is not yet "pending" (should be null) until profile sync
	res = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+badIdentifier+suffix, nil, http.StatusBadRequest, nil, "operation",
		"PKIOperation", "message", message)
	errBody, err = io.ReadAll(res.Body)
	require.NoError(t, err)
	assert.Contains(t, string(errBody), "profile status")

	// trigger a profile sync
	s.awaitTriggerProfileSchedule(t)
	// Good request with non-expired challenge
	res = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+identifier+suffix, nil, http.StatusOK, nil, "operation",
		"PKIOperation", "message", message)
	body, err = io.ReadAll(res.Body)
	require.NoError(t, err)
	pkiMessage, err = scep.ParsePKIMessage(body, scep.WithCACerts(certs))
	require.NoError(t, err)
	assert.Equal(t, scep.CertRep, pkiMessage.MessageType)
}

func (s *integrationMDMTestSuite) TestSmallstepSCEPProxy() {
	s.runSmallstepSCEPProxyTestWithOptionalSuffix("")
}

func (s *integrationMDMTestSuite) TestSmallstepSCEPProxyWithMicrosoftSuffix() {
	// Windows SCEP client appends pkiclient.exe to the URL for all requests
	s.runSmallstepSCEPProxyTestWithOptionalSuffix("/pkiclient.exe")
}

func (s *integrationMDMTestSuite) runSmallstepSCEPProxyTestWithOptionalSuffix(suffix string) {
	t := s.T()
	ctx := context.Background()
	caName := "Smallstep" + strings.ReplaceAll(strings.ReplaceAll(suffix, "/", ""), ".", "")

	// Add an MDM profile
	globalProfiles := [][]byte{
		mobileconfigForTest("N1", "Good1"),
		mobileconfigForTest("N2", "Bad1"),
	}
	// add global profile
	s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: globalProfiles}, http.StatusNoContent)

	// Create a host and then enroll to MDM.
	host, mdmDevice := createHostThenEnrollMDM(s.ds, s.server.URL, t)
	setupPusher(s, t, mdmDevice)
	// trigger a profile sync
	s.awaitTriggerProfileSchedule(t)
	profiles, err := s.ds.GetHostMDMAppleProfiles(ctx, host.UUID)
	require.NoError(t, err)
	require.GreaterOrEqual(t, len(profiles), 2)
	var profileUUID string
	var goodProfile, badProfile fleet.HostMDMAppleProfile
	for _, profile := range profiles {
		switch profile.Identifier {
		case "Good1":
			profileUUID = profile.ProfileUUID
			goodProfile = profile
		case "Bad1":
			profile.Status = &fleet.MDMDeliveryFailed
			badProfile = profile
		}
	}
	err = s.ds.BulkUpsertMDMAppleHostProfiles(ctx, []*fleet.MDMAppleBulkUpsertHostProfilePayload{{
		ProfileUUID:       badProfile.ProfileUUID,
		ProfileIdentifier: badProfile.Identifier,
		ProfileName:       badProfile.Name,
		HostUUID:          host.UUID,
		CommandUUID:       badProfile.CommandUUID,
		OperationType:     badProfile.OperationType,
		Status:            badProfile.Status,
		Detail:            badProfile.Detail,
		Checksum:          []byte("checksum"),
		Scope:             fleet.PayloadScopeSystem,
	}, {
		ProfileUUID:       goodProfile.ProfileUUID,
		ProfileIdentifier: goodProfile.Identifier,
		ProfileName:       goodProfile.Name,
		HostUUID:          host.UUID,
		CommandUUID:       goodProfile.CommandUUID,
		OperationType:     goodProfile.OperationType,
		Status:            goodProfile.Status,
		Detail:            goodProfile.Detail,
		Checksum:          []byte("checksum"),
		Scope:             fleet.PayloadScopeSystem,
	}})
	require.NoError(t, err)
	err = s.ds.BulkUpsertMDMManagedCertificates(ctx, []*fleet.MDMManagedCertificate{
		{
			HostUUID:    host.UUID,
			ProfileUUID: profileUUID,
			Type:        fleet.CAConfigSmallstep,
			CAName:      caName,
		},
		{
			HostUUID:    host.UUID,
			ProfileUUID: badProfile.ProfileUUID,
			Type:        fleet.CAConfigSmallstep,
			CAName:      caName,
		},
	})
	require.NoError(t, err)

	identifier := url.PathEscape(host.UUID + "," + profileUUID + "," + caName)
	badIdentifier := url.PathEscape(host.UUID + "," + badProfile.ProfileUUID + "," + caName)

	data, err := os.ReadFile("./testdata/PKCSReq.der")
	require.NoError(t, err)
	message := base64.StdEncoding.EncodeToString(data)

	// NDES not configured
	res := s.DoRawNoAuth("GET", apple_mdm.SCEPProxyPath+identifier+suffix, nil, http.StatusBadRequest)
	errBody, err := io.ReadAll(res.Body)
	require.NoError(t, err)
	assert.Contains(t, string(errBody), "missing operation")
	// Provide SCEP operation (GetCACaps)
	res = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+identifier+suffix, nil, http.StatusBadRequest, nil, "operation", "GetCACaps")
	errBody, err = io.ReadAll(res.Body)
	require.NoError(t, err)
	assert.Contains(t, string(errBody), eeservice.MessageSCEPProxyNotConfigured)
	// Provide SCEP operation (GetCACerts)
	res = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+identifier+suffix, nil, http.StatusBadRequest, nil, "operation", "GetCACert")
	errBody, err = io.ReadAll(res.Body)
	require.NoError(t, err)
	assert.Contains(t, string(errBody), eeservice.MessageSCEPProxyNotConfigured)
	// Provide SCEP operation (PKIOperation)
	res = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+identifier+suffix, nil, http.StatusBadRequest, nil, "operation", "PKIOperation",
		"message", message)
	errBody, err = io.ReadAll(res.Body)
	require.NoError(t, err)
	assert.Contains(t, string(errBody), eeservice.MessageSCEPProxyNotConfigured)
	// Provide SCEP operation (GetNextCACert)
	res = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+identifier+suffix, nil, http.StatusBadRequest, nil, "operation", "GetNextCACert")
	errBody, err = io.ReadAll(res.Body)
	require.NoError(t, err)
	assert.Contains(t, string(errBody), "not implemented")

	// Configure a bad SCEP URL using local test server
	testServer, cleanup := createTestServerWithStatusCode(http.StatusGone)
	defer cleanup()

	// Bypass service validation to insert a bad url
	challengeUrl := "bad-admin-url"
	username := "user"
	password := "pass"
	ca, err := s.ds.NewCertificateAuthority(ctx, &fleet.CertificateAuthority{
		Type:         string(fleet.CATypeSmallstep),
		Name:         ptr.String(caName),
		URL:          &testServer.URL,
		ChallengeURL: &challengeUrl,
		Username:     &username,
		Password:     &password,
	})
	require.NoError(t, err)

	res = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+identifier+suffix, nil, http.StatusInternalServerError, nil, "operation", "GetCACaps")
	errBody, err = io.ReadAll(res.Body)
	require.NoError(t, err)
	assert.Contains(t, string(errBody), "Could not GetCACaps from SCEP server")
	res = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+identifier+suffix, nil, http.StatusInternalServerError, nil, "operation", "GetCACert")
	errBody, err = io.ReadAll(res.Body)
	require.NoError(t, err)
	assert.Contains(t, string(errBody), "Could not GetCACert from SCEP server")
	res = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+identifier+suffix, nil, http.StatusInternalServerError, nil, "operation",
		"PKIOperation", "message", message)
	errBody, err = io.ReadAll(res.Body)
	require.NoError(t, err)
	assert.Contains(t, string(errBody), "Could not do PKIOperation on SCEP server")

	// Test timeout error
	ndesTimeoutServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
		time.Sleep(1 * time.Second)
		w.WriteHeader(http.StatusOK)
	}))
	*s.scepConfig.Timeout = time.Microsecond
	t.Cleanup(func() {
		*s.scepConfig.Timeout = 10 * time.Second
		ndesTimeoutServer.Close()
	})

	// Bypass service validation to verify timeout behaviour
	s.DoRaw("DELETE", fmt.Sprintf("/api/latest/fleet/certificate_authorities/%d", ca.ID), nil, http.StatusNoContent)
	ca, err = s.ds.NewCertificateAuthority(ctx, &fleet.CertificateAuthority{
		Type:         string(fleet.CATypeSmallstep),
		Name:         ptr.String(caName),
		URL:          &ndesTimeoutServer.URL,
		ChallengeURL: &challengeUrl,
		Username:     &username,
		Password:     &password,
	})
	require.NoError(t, err)

	// GetCACaps
	_ = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+identifier+suffix, nil, http.StatusRequestTimeout, nil, "operation", "GetCACaps")
	// GetCACert
	_ = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+identifier+suffix, nil, http.StatusRequestTimeout, nil, "operation", "GetCACert")
	// PKIOperation
	_ = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+identifier+suffix, nil, http.StatusRequestTimeout, nil, "operation",
		"PKIOperation", "message", message)
	*s.scepConfig.Timeout = 10 * time.Second

	scepServer := scep_server.StartTestSCEPServer(t)

	s.DoRaw("DELETE", fmt.Sprintf("/api/latest/fleet/certificate_authorities/%d", ca.ID), nil, http.StatusNoContent)
	_, err = s.ds.NewCertificateAuthority(ctx, &fleet.CertificateAuthority{
		Type:         string(fleet.CATypeSmallstep),
		Name:         ptr.String(caName),
		URL:          ptr.String(scepServer.URL + "/scep"),
		ChallengeURL: &challengeUrl,
		Username:     &username,
		Password:     &password,
	})
	require.NoError(t, err)

	// GetCACaps
	res = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+identifier+suffix, nil, http.StatusOK, nil, "operation", "GetCACaps")
	body, err := io.ReadAll(res.Body)
	require.NoError(t, err)
	assert.Equal(t, scepserver.DefaultCACaps, string(body))

	// GetCACert
	res = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+identifier+suffix, nil, http.StatusOK, nil, "operation", "GetCACert")
	body, err = io.ReadAll(res.Body)
	require.NoError(t, err)
	certs, err := x509.ParseCertificates(body)
	require.NoError(t, err)
	assert.Len(t, certs, 1)

	// PKIOperation
	// Invalid identifier format
	res = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+"bozo"+suffix, nil, http.StatusBadRequest, nil, "operation",
		"PKIOperation", "message", message)
	errBody, err = io.ReadAll(res.Body)
	require.NoError(t, err)
	assert.Contains(t, string(errBody), "invalid identifier")
	// Non-Apple config profile (missing leading 'a')
	res = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+"bozoHost%2CbozoProfile"+suffix, nil, http.StatusBadRequest, nil, "operation",
		"PKIOperation", "message", message)
	errBody, err = io.ReadAll(res.Body)
	require.NoError(t, err)
	assert.Contains(t, string(errBody), "invalid profile UUID")
	// Unknown host/profile
	res = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+"bozoHost%2CabozoProfile"+suffix, nil, http.StatusBadRequest, nil, "operation",
		"PKIOperation", "message", message)
	errBody, err = io.ReadAll(res.Body)
	require.NoError(t, err)
	assert.Contains(t, string(errBody), "unknown identifier")
	// Profile which is not pending
	res = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+badIdentifier+suffix, nil, http.StatusBadRequest, nil, "operation",
		"PKIOperation", "message", message)
	errBody, err = io.ReadAll(res.Body)
	require.NoError(t, err)
	assert.Contains(t, string(errBody), "profile status")

	// "Good" request without one-time challenge.
	// Note, a cert is not returned here because the message is not a fully valid SCEP request. However, building a valid SCEP request is a bit involved,
	// and this request is sufficient for testing our proxy functionality.
	res = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+identifier+suffix, nil, http.StatusOK, nil, "operation",
		"PKIOperation", "message", message)
	body, err = io.ReadAll(res.Body)
	require.NoError(t, err)
	pkiMessage, err := scep.ParsePKIMessage(body, scep.WithCACerts(certs))
	require.NoError(t, err)
	assert.Equal(t, scep.CertRep, pkiMessage.MessageType)

	// Expired challenge
	err = s.ds.BulkUpsertMDMManagedCertificates(ctx, []*fleet.MDMManagedCertificate{
		{
			HostUUID:             host.UUID,
			ProfileUUID:          profileUUID,
			ChallengeRetrievedAt: ptr.Time(time.Now().Add(-eeservice.SmallstepChallengeInvalidAfter)),
			Type:                 fleet.CAConfigSmallstep,
			CAName:               caName,
		},
	})
	require.NoError(t, err)
	res = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+identifier+suffix, nil, http.StatusBadRequest, nil, "operation",
		"PKIOperation", "message", message)
	errBody, err = io.ReadAll(res.Body)
	require.NoError(t, err)
	assert.Contains(t, string(errBody), "challenge password")

	// Non-expired challenge
	err = s.ds.BulkUpsertMDMManagedCertificates(ctx, []*fleet.MDMManagedCertificate{
		{
			HostUUID:             host.UUID,
			ProfileUUID:          profileUUID,
			ChallengeRetrievedAt: ptr.Time(time.Now().Add(-eeservice.SmallstepChallengeInvalidAfter + time.Minute)),
			Type:                 fleet.CAConfigSmallstep,
			CAName:               caName,
		},
	})
	require.NoError(t, err)

	// Profile status is not yet "pending" (should be null) until profile sync
	res = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+badIdentifier+suffix, nil, http.StatusBadRequest, nil, "operation",
		"PKIOperation", "message", message)
	errBody, err = io.ReadAll(res.Body)
	require.NoError(t, err)
	assert.Contains(t, string(errBody), "profile status")

	// trigger a profile sync
	s.awaitTriggerProfileSchedule(t)
	// Good request with non-expired challenge
	res = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+identifier+suffix, nil, http.StatusOK, nil, "operation",
		"PKIOperation", "message", message)
	body, err = io.ReadAll(res.Body)
	require.NoError(t, err)
	pkiMessage, err = scep.ParsePKIMessage(body, scep.WithCACerts(certs))
	require.NoError(t, err)
	assert.Equal(t, scep.CertRep, pkiMessage.MessageType)
}

func (s *integrationMDMTestSuite) TestDigiCertConfig() {
	t := s.T()
	ctx := context.Background()
	digiCertServer := createMockDigiCertServer(t)

	// Add DigiCert integration with bad URL using local test server
	testServer, cleanup := createTestServerWithStatusCode(http.StatusGone)
	defer cleanup()

	caBad := newMockDigicertCA(testServer.URL, "ca")
	certPayload := createCertificateAuthorityRequest{
		CertificateAuthorityPayload: fleet.CertificateAuthorityPayload{
			DigiCert: &caBad,
		},
	}
	res := createCertificateAuthorityResponse{}
	s.DoJSON("POST", "/api/latest/fleet/certificate_authorities", &certPayload, http.StatusBadRequest, &res)

	// Add 3 DigiCert integrations
	ca0 := newMockDigicertCA(digiCertServer.server.URL, "ca0")
	ca0.APIToken = "api_token0"
	ca1 := newMockDigicertCA(digiCertServer.server.URL, "ca1")
	ca1.APIToken = "api_token1"
	ca2 := newMockDigicertCA(digiCertServer.server.URL, "ca2")
	ca2.APIToken = "api_token2"
	certPayload.CertificateAuthorityPayload.DigiCert = &ca0
	s.DoJSON("POST", "/api/latest/fleet/certificate_authorities", &certPayload, http.StatusOK, &res)
	certPayload.CertificateAuthorityPayload.DigiCert = &ca1
	s.DoJSON("POST", "/api/latest/fleet/certificate_authorities", &certPayload, http.StatusOK, &res)
	certPayload.CertificateAuthorityPayload.DigiCert = &ca2
	s.DoJSON("POST", "/api/latest/fleet/certificate_authorities", &certPayload, http.StatusOK, &res)

	listRes := listCertificateAuthoritiesResponse{}
	s.DoJSON("GET", "/api/latest/fleet/certificate_authorities", &listCertificateAuthoritiesRequest{}, http.StatusOK, &listRes)

	digiCerts := []*fleet.CertificateAuthority{}
	digiCertsWithSecrets := []*fleet.CertificateAuthority{}
	for _, ca := range listRes.CertificateAuthorities {
		if ca.Type == string(fleet.CATypeDigiCert) {
			getRes := getCertificateAuthorityResponse{}
			s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/certificate_authorities/%d", ca.ID), &getCertificateAuthorityRequest{}, http.StatusOK, &getRes)
			digiCerts = append(digiCerts, getRes.CertificateAuthority)

			caWithSecret, err := s.ds.GetCertificateAuthorityByID(ctx, ca.ID, true)
			require.NoError(t, err)
			digiCertsWithSecrets = append(digiCertsWithSecrets, caWithSecret)
		}
	}
	assert.Len(t, digiCerts, 3)

	for _, ca := range digiCerts {
		assert.Equal(t, fleet.MaskedPassword, *ca.APIToken)
	}

	require.Len(t, digiCertsWithSecrets, 3)
	assert.EqualValues(t, "api_token0", *digiCertsWithSecrets[0].APIToken)
	assert.EqualValues(t, "api_token1", *digiCertsWithSecrets[1].APIToken)
	assert.EqualValues(t, "api_token2", *digiCertsWithSecrets[2].APIToken)

	// Check 3 added activities are present
	var listActivities listActivitiesResponse
	s.DoJSON("GET", "/api/latest/fleet/activities", nil, http.StatusOK,
		&listActivities, "order_key", "a.id", "order_direction", "desc", "per_page", "10")
	require.True(t, len(listActivities.Activities) > 0)
	activity := fleet.ActivityAddedDigiCert{}
	caNames := make([]string, 0, 3)
	for _, act := range listActivities.Activities {
		if act.Type == activity.ActivityName() {
			err := json.Unmarshal(*act.Details, &activity)
			require.NoError(t, err)
			caNames = append(caNames, activity.Name)
			if len(caNames) == 3 {
				break
			}
		}
	}
	slices.Sort(caNames)
	assert.EqualValues(t, caNames, []string{"ca0", "ca1", "ca2"})

	// TODO(HCA): Re enable and switch out with endpoints once update endpoint is implemented.
	/*
		// Add 1, modify 1, delete 1, keep 1 the same (DigiCert integrations)
		ca1.URL += "//"
		ca3 := getDigiCertIntegration(digiCertServer.server.URL, "ca3")
		ca3.APIToken = "api_token3"
		appConfig = map[string]interface{}{
			"integrations": map[string]interface{}{
				"digicert": []fleet.DigiCertCA{ca3, ca2, ca1},
			},
		}
		raw, err = json.Marshal(appConfig)
		require.NoError(t, err)
		req.RawMessage = raw
		res = appConfigResponse{}
		s.DoJSON("PATCH", "/api/latest/fleet/config", &req, http.StatusOK, &res)
		require.Len(t, res.Integrations.DigiCert.Value, 3)
		assert.NotEqual(t, res.Integrations.DigiCert.Value[0].Name, res.Integrations.DigiCert.Value[1].Name)
		assert.NotEqual(t, res.Integrations.DigiCert.Value[1].Name, res.Integrations.DigiCert.Value[2].Name)
		for _, ca := range res.Integrations.DigiCert.Value {
			switch ca.Name {
			case "ca1":
				assert.True(t, ca.Equals(&ca1))
			case "ca2":
				assert.True(t, ca.Equals(&ca2))
			case "ca3":
				assert.True(t, ca.Equals(&ca3))
			default:
				t.Fatalf("unexpected ca name: %s", ca.Name)
			}
			assert.Equal(t, ca.APIToken, fleet.MaskedPassword)
		}
		assets, err = s.ds.GetAllCAConfigAssetsByType(ctx, fleet.CAConfigDigiCert)
		require.NoError(t, err)
		require.Len(t, assets, 3)
		assert.EqualValues(t, "api_token1", assets["ca1"].Value)
		assert.EqualValues(t, "api_token2", assets["ca2"].Value)
		assert.EqualValues(t, "api_token3", assets["ca3"].Value)

		listActivities = listActivitiesResponse{}
		s.DoJSON("GET", "/api/latest/fleet/activities", nil, http.StatusOK,
			&listActivities, "order_key", "a.id", "order_direction", "desc", "per_page", "10")
		require.True(t, len(listActivities.Activities) > 0)
		activity = fleet.ActivityAddedDigiCert{}
		editActivity := fleet.ActivityEditedDigiCert{}
		delActivity := fleet.ActivityDeletedDigiCert{}
		var numFound int
		for _, act := range listActivities.Activities {
			switch act.Type {
			case activity.ActivityName():
				err := json.Unmarshal(*act.Details, &activity)
				require.NoError(t, err)
				assert.Equal(t, activity.Name, ca3.Name)
				numFound++
			case editActivity.ActivityName():
				err := json.Unmarshal(*act.Details, &editActivity)
				require.NoError(t, err)
				assert.Equal(t, editActivity.Name, ca1.Name)
				numFound++
			case delActivity.ActivityName():
				err := json.Unmarshal(*act.Details, &delActivity)
				require.NoError(t, err)
				assert.Equal(t, delActivity.Name, ca0.Name)
				numFound++
			}
			if numFound == 3 {
				break
			}
		}
		assert.Equal(t, 3, numFound)
	*/

	// Clear DigiCert integrations
	for _, ca := range digiCerts {
		s.DoRaw("DELETE", fmt.Sprintf("/api/latest/fleet/certificate_authorities/%d", ca.ID), nil, http.StatusNoContent)
	}

	// TODO(HCA): Re enable and switch out with endpoints once update endpoint is implemented.
	/*
		// Check that 3 deleted activities are present
		listActivities = listActivitiesResponse{}
		s.DoJSON("GET", "/api/latest/fleet/activities", nil, http.StatusOK,
			&listActivities, "order_key", "a.id", "order_direction", "desc", "per_page", "10")
		require.True(t, len(listActivities.Activities) > 0)
		delActivity := fleet.ActivityDeletedDigiCert{}
		caNames = make([]string, 0, 3)
		for _, act := range listActivities.Activities {
			if act.Type == delActivity.ActivityName() {
				err := json.Unmarshal(*act.Details, &delActivity)
				require.NoError(t, err)
				caNames = append(caNames, delActivity.Name)
				if len(caNames) == 3 {
					break
				}
			}
		}
		slices.Sort(caNames)
		assert.EqualValues(t, caNames, []string{"ca1", "ca2", "ca3"})
	*/
}

func (s *integrationMDMTestSuite) TestDigiCertIntegration() {
	t := s.T()
	ctx := context.Background()
	s.setSkipWorkerJobs(t)
	digiCertServer := createMockDigiCertServer(t)

	appConfig, err := s.ds.AppConfig(ctx)
	require.NoError(t, err)
	appConfig.MDM.MacOSSetup = fleet.MacOSSetup{
		EnableEndUserAuthentication: false,
	}
	err = s.ds.SaveAppConfig(ctx, appConfig)
	require.NoError(t, err)

	// Add DigiCert config
	ca := newMockDigicertCA(digiCertServer.server.URL, "my_CA")
	ca.APIToken = "api_token0"
	certPayload := createCertificateAuthorityRequest{
		CertificateAuthorityPayload: fleet.CertificateAuthorityPayload{
			DigiCert: &ca,
		},
	}
	res := createCertificateAuthorityResponse{}
	s.DoJSON("POST", "/api/latest/fleet/certificate_authorities", &certPayload, http.StatusOK, &res)

	// Create a host and then enroll to MDM.
	host, mdmDevice := createHostThenEnrollMDM(s.ds, s.server.URL, t)
	setupPusher(s, t, mdmDevice)
	// trigger a profile sync
	s.awaitTriggerProfileSchedule(t)
	profiles, err := s.ds.GetHostMDMAppleProfiles(ctx, host.UUID)
	require.NoError(t, err)
	require.GreaterOrEqual(t, len(profiles), 0)

	// Receive enrollment profiles (we are not checking/testing these here)
	for {
		cmd, err := mdmDevice.Idle()
		require.NoError(t, err)
		if cmd == nil {
			break
		}
		_, err = mdmDevice.Acknowledge(cmd.CommandUUID)
		require.NoError(t, err)
	}

	// Add a profile with a bad CA
	profile := digiCertForTest("N1", "BadCA", "badName")
	rawRes := s.Do("POST", "/api/latest/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
		{Name: "N1", Contents: profile},
	}}, http.StatusBadRequest)
	errMsg := extractServerErrorText(rawRes.Body)
	require.Contains(t, errMsg, "_badName does not exist")

	// ////////////////////////////////
	// Test a good profile -- happy path
	profile = digiCertForTest("N2", "I2", "my_CA")
	s.Do("POST", "/api/latest/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
		{Name: "N2", Contents: profile},
	}}, http.StatusNoContent)
	profiles, err = s.ds.GetHostMDMAppleProfiles(ctx, host.UUID)
	require.NoError(t, err)
	require.GreaterOrEqual(t, len(profiles), 1)

	// trigger a profile sync
	s.awaitTriggerProfileSchedule(t)
	p := s.assertConfigProfilesByIdentifier(nil, "I2", true)
	require.Contains(t, string(p.Mobileconfig), "com.fleetdm.pkcs12")

	cmd, err := mdmDevice.Idle()
	require.NoError(t, err)
	require.NotNil(t, cmd, "Expecting PKCS12 certificate")
	var fullCmd micromdm.CommandPayload
	require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd))
	cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID)
	require.NoError(t, err)
	assert.Nil(t, cmd)
	require.NotNil(t, fullCmd.Command)
	require.NotNil(t, fullCmd.Command.InstallProfile)
	rawProfile := fullCmd.Command.InstallProfile.Payload
	if !bytes.HasPrefix(rawProfile, []byte("<?xml")) {
		p7, err := pkcs7.Parse(rawProfile)
		require.NoError(t, err)
		require.NoError(t, p7.Verify())
		rawProfile = p7.Content
	}

	type pkcs12Profile struct {
		PayloadContent []map[string]interface{} `plist:"PayloadContent"`
	}
	var pkcs12Prof pkcs12Profile
	require.NoError(t, plist.Unmarshal(rawProfile, &pkcs12Prof))
	assert.Equal(t, "com.apple.security.pkcs12", pkcs12Prof.PayloadContent[0]["PayloadType"].(string))
	password := pkcs12Prof.PayloadContent[0]["Password"].(string)
	data := pkcs12Prof.PayloadContent[0]["PayloadContent"].([]byte)
	_, certificate, err := pkcs12.Decode(data, password)
	require.NoError(t, err)
	assert.Equal(t, ca.CertificateCommonName, certificate.Subject.CommonName)

	// Check request to DigiCert
	digiCertServer.certReqMu.Lock()
	assert.Equal(t, ca.ProfileID, digiCertServer.certReq.Profile["id"])
	assert.Equal(t, ca.CertificateSeatID, digiCertServer.certReq.Seat["seat_id"])
	assert.Equal(t, "x509", digiCertServer.certReq.DeliveryFormat)
	assert.Equal(t, ca.CertificateCommonName, digiCertServer.certReq.Attributes["subject"].(map[string]interface{})["common_name"])

	// Need to convert UPNs to string slice since assert.Equal cannot compare string slice and interface slice
	upnsReceived := digiCertServer.certReq.Attributes["extensions"].(map[string]interface{})["san"].(map[string]interface{})["user_principal_names"].([]interface{})
	var stringSlice []string
	for _, item := range upnsReceived {
		if str, ok := item.(string); ok {
			stringSlice = append(stringSlice, str)
		}
	}
	assert.Equal(t, ca.CertificateUserPrincipalNames, stringSlice)
	digiCertServer.certReqMu.Unlock()
	certProf, err := s.ds.GetAppleHostMDMCertificateProfile(ctx, host.UUID, p.ProfileUUID, "my_CA")
	require.NoError(t, err)
	require.NotNil(t, certProf.NotValidAfter)
	assert.Equal(t, digiCertServer.notAfter, *certProf.NotValidAfter)

	// ////////////////////////////////
	// Try a DigiCert CA that will fail
	caFail := newMockDigicertCA(digiCertServer.server.URL, "fail_CA")
	caFail.CertificateCommonName = "Fail"
	certPayload = createCertificateAuthorityRequest{
		CertificateAuthorityPayload: fleet.CertificateAuthorityPayload{
			DigiCert: &caFail,
		},
	}
	resFail := createCertificateAuthorityResponse{}
	s.DoJSON("POST", "/api/latest/fleet/certificate_authorities", &certPayload, http.StatusOK, &resFail)

	profileFail := digiCertForTest("N3", "I3", "fail_CA")
	s.Do("POST", "/api/latest/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
		{Name: "N2", Contents: profile}, // resend the previous profile so it doesn't get removed
		{Name: "N3", Contents: profileFail},
	}}, http.StatusNoContent)
	profiles, err = s.ds.GetHostMDMAppleProfiles(ctx, host.UUID)
	require.NoError(t, err)
	require.GreaterOrEqual(t, len(profiles), 2)

	s.awaitTriggerProfileSchedule(t)
	p = s.assertConfigProfilesByIdentifier(nil, "I3", true)
	require.Contains(t, string(p.Mobileconfig), "com.fleetdm.pkcs12")

	getHostResp := getDeviceHostResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &getHostResp)
	var foundGood, foundFailed bool
	require.NotNil(t, getHostResp.Host.MDM.Profiles)
	for _, prof := range *getHostResp.Host.MDM.Profiles {
		if prof.Name == "N2" {
			foundGood = true
			assert.Equal(t, fleet.MDMDeliveryVerifying, *prof.Status)
			assert.Equal(t, fleet.MDMOperationTypeInstall, prof.OperationType)
			continue
		}
		if prof.Name == "N3" {
			foundFailed = true
			assert.Equal(t, fleet.MDMDeliveryFailed, *prof.Status)
			assert.Equal(t, fleet.MDMOperationTypeInstall, prof.OperationType)
			assert.Contains(t, prof.Detail, "Expected Fail")
			continue
		}
	}
	assert.True(t, foundGood)
	assert.True(t, foundFailed)

	// ////////////////////////////////
	// Test DigiCert CA with Fleet variables
	mysql.ExecAdhocSQL(t, s.ds, func(db sqlx.ExtContext) error {
		_, err := db.ExecContext(
			context.Background(),
			`INSERT INTO host_emails (host_id, email, source) VALUES (?, ?, ?)`,
			host.ID, "idp@example.com", "mdm_idp_accounts")

		return err
	})
	caFleetVars := fleet.DigiCertCA{
		Name:                          "FleetVars",
		URL:                           digiCertServer.server.URL,
		APIToken:                      "api_token",
		ProfileID:                     "profile_id",
		CertificateCommonName:         "$FLEET_VAR_" + string(fleet.FleetVarHostHardwareSerial) + " $FLEET_VAR_" + string(fleet.FleetVarHostEndUserEmailIDP),
		CertificateUserPrincipalNames: []string{"_${FLEET_VAR_" + string(fleet.FleetVarHostEndUserEmailIDP) + "}_"},
		CertificateSeatID:             "_${FLEET_VAR_" + string(fleet.FleetVarHostHardwareSerial) + "}_",
	}
	certPayload = createCertificateAuthorityRequest{
		CertificateAuthorityPayload: fleet.CertificateAuthorityPayload{
			DigiCert: &caFleetVars,
		},
	}
	resFleetVars := createCertificateAuthorityResponse{}
	s.DoJSON("POST", "/api/latest/fleet/certificate_authorities", &certPayload, http.StatusOK, &resFleetVars)

	profileFleetVars := digiCertForTest("N4", "I4", "FleetVars")
	s.Do("POST", "/api/latest/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
		{Name: "N2", Contents: profile}, // resend the previous profile so it doesn't get removed
		{Name: "N4", Contents: profileFleetVars},
	}}, http.StatusNoContent)
	profiles, err = s.ds.GetHostMDMAppleProfiles(ctx, host.UUID)
	require.NoError(t, err)
	require.GreaterOrEqual(t, len(profiles), 2)

	// trigger a profile sync
	s.awaitTriggerProfileSchedule(t)
	p = s.assertConfigProfilesByIdentifier(nil, "I4", true)
	require.Contains(t, string(p.Mobileconfig), "com.fleetdm.pkcs12")

	// Check that status is pending (and not failed)
	getHostResp = getDeviceHostResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &getHostResp)
	require.NotNil(t, getHostResp.Host.MDM.Profiles)
	var found bool
	for _, prof := range *getHostResp.Host.MDM.Profiles {
		if prof.Name == "N4" {
			found = true
			assert.Equal(t, fleet.MDMDeliveryPending, *prof.Status)
			assert.Equal(t, fleet.MDMOperationTypeInstall, prof.OperationType)
			assert.Empty(t, prof.Detail)
			continue
		}
	}
	assert.True(t, found)

	cmd, err = mdmDevice.Idle()
	require.NoError(t, err)
	// first command is the RemoveProfile of N3 (which was install failed)
	require.NotNil(t, cmd, "Expecting removal of failed N3 install")
	require.Equal(t, "RemoveProfile", cmd.Command.RequestType)

	// second command is the InstallProfile of N4
	cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID)
	require.NoError(t, err)
	require.NotNil(t, cmd, "Expecting PKCS12 certificate")
	require.Equal(t, "InstallProfile", cmd.Command.RequestType)
	fullCmd = micromdm.CommandPayload{}
	require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd))

	cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID)
	require.NoError(t, err)
	assert.Nil(t, cmd)

	require.NotNil(t, fullCmd.Command)
	require.NotNil(t, fullCmd.Command.InstallProfile)
	rawProfile = fullCmd.Command.InstallProfile.Payload
	if !bytes.HasPrefix(rawProfile, []byte("<?xml")) {
		p7, err := pkcs7.Parse(rawProfile)
		require.NoError(t, err)
		require.NoError(t, p7.Verify())
		rawProfile = p7.Content
	}

	pkcs12Prof = pkcs12Profile{}
	require.NoError(t, plist.Unmarshal(rawProfile, &pkcs12Prof))
	assert.Equal(t, "com.apple.security.pkcs12", pkcs12Prof.PayloadContent[0]["PayloadType"].(string))
	password = pkcs12Prof.PayloadContent[0]["Password"].(string)
	data = pkcs12Prof.PayloadContent[0]["PayloadContent"].([]byte)
	_, certificate, err = pkcs12.Decode(data, password)
	require.NoError(t, err)
	assert.Equal(t, host.HardwareSerial+" idp@example.com", certificate.Subject.CommonName)

	// Check request to DigiCert
	digiCertServer.certReqMu.Lock()
	assert.Equal(t, caFleetVars.ProfileID, digiCertServer.certReq.Profile["id"])
	assert.Equal(t, "_"+host.HardwareSerial+"_", digiCertServer.certReq.Seat["seat_id"])
	assert.Equal(t, "x509", digiCertServer.certReq.DeliveryFormat)
	assert.Equal(t, host.HardwareSerial+" idp@example.com", digiCertServer.certReq.Attributes["subject"].(map[string]interface{})["common_name"])

	// Need to convert UPNs to string slice since assert.Equal cannot compare string slice and interface slice
	upnsReceived = digiCertServer.certReq.Attributes["extensions"].(map[string]interface{})["san"].(map[string]interface{})["user_principal_names"].([]interface{})
	stringSlice = nil
	for _, item := range upnsReceived {
		if str, ok := item.(string); ok {
			stringSlice = append(stringSlice, str)
		}
	}
	assert.Equal(t, []string{"_idp@example.com_"}, stringSlice)
	digiCertServer.certReqMu.Unlock()

	// ////////////////////////////////
	// Test a good profile with 2 DigiCert payloads -- happy path
	s.Do("POST", "/api/latest/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
		{Name: "N2", Contents: profile},          // resend the previous profile so it doesn't get removed
		{Name: "N4", Contents: profileFleetVars}, // resend the previous profile so it doesn't get removed
		{Name: "DigiCert2", Contents: digiCert2Mobileconfig},
	}}, http.StatusNoContent)
	profiles, err = s.ds.GetHostMDMAppleProfiles(ctx, host.UUID)
	require.NoError(t, err)
	require.GreaterOrEqual(t, len(profiles), 3)
	p = s.assertConfigProfilesByIdentifier(nil, "DigiCert2", true)

	// trigger a profile sync
	s.awaitTriggerProfileSchedule(t)

	cmd, err = mdmDevice.Idle()
	require.NoError(t, err)
	require.NotNil(t, cmd, "Expecting PKCS12 certificate")
	fullCmd = micromdm.CommandPayload{}
	require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd))
	cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID)
	require.NoError(t, err)
	assert.Nil(t, cmd)
	require.NotNil(t, fullCmd.Command)
	require.NotNil(t, fullCmd.Command.InstallProfile)
	rawProfile = fullCmd.Command.InstallProfile.Payload
	if !bytes.HasPrefix(rawProfile, []byte("<?xml")) {
		p7, err := pkcs7.Parse(rawProfile)
		require.NoError(t, err)
		require.NoError(t, p7.Verify())
		rawProfile = p7.Content
	}

	pkcs12Prof = pkcs12Profile{}
	require.NoError(t, plist.Unmarshal(rawProfile, &pkcs12Prof))
	require.Len(t, pkcs12Prof.PayloadContent, 2)
	assert.Equal(t, "com.apple.security.pkcs12", pkcs12Prof.PayloadContent[0]["PayloadType"].(string))
	assert.Equal(t, "com.apple.security.pkcs12", pkcs12Prof.PayloadContent[1]["PayloadType"].(string))
	password = pkcs12Prof.PayloadContent[0]["Password"].(string)
	data = pkcs12Prof.PayloadContent[0]["PayloadContent"].([]byte)
	_, certificate, err = pkcs12.Decode(data, password)
	require.NoError(t, err)
	assert.Equal(t, ca.CertificateCommonName, certificate.Subject.CommonName)
	password = pkcs12Prof.PayloadContent[1]["Password"].(string)
	data = pkcs12Prof.PayloadContent[1]["PayloadContent"].([]byte)
	_, certificate, err = pkcs12.Decode(data, password)
	require.NoError(t, err)
	assert.Equal(t, host.HardwareSerial+" idp@example.com", certificate.Subject.CommonName)

	prof, err := s.ds.GetAppleHostMDMCertificateProfile(ctx, host.UUID, p.ProfileUUID, "my_CA")
	require.NoError(t, err)
	require.NotNil(t, prof)
	assert.NotNil(t, prof.NotValidAfter)
	assert.Equal(t, fleet.CAConfigDigiCert, prof.Type)
	assert.Equal(t, fleet.MDMDeliveryVerifying, *prof.Status)
	prof, err = s.ds.GetAppleHostMDMCertificateProfile(ctx, host.UUID, p.ProfileUUID, "FleetVars")
	require.NoError(t, err)
	require.NotNil(t, prof)
	assert.NotNil(t, prof.NotValidAfter)
	assert.Equal(t, fleet.CAConfigDigiCert, prof.Type)
	assert.Equal(t, fleet.MDMDeliveryVerifying, *prof.Status)

	// ////////////////////////////////
	// Modify the CN/UPN/Seat ID of the profile -- DigiCert verification should not happen
	digiCertServer.server.Close() // so that verify fails if attempted
	caFleetVars.CertificateCommonName = "common_name"
	caFleetVars.CertificateUserPrincipalNames = nil
	caFleetVars.CertificateSeatID = "seat_id"
	// TODO(HCA): Call update endpoint to follow logic below once implemented
	/* appConfig = map[string]interface{}{
		"integrations": map[string]interface{}{
			"digicert": []fleet.DigiCertCA{ca, caFail, caFleetVars},
		},
	}
	raw, err = json.Marshal(appConfig)
	require.NoError(t, err)
	req = modifyAppConfigRequest{}
	req.RawMessage = raw
	res = appConfigResponse{}
	s.DoJSON("PATCH", "/api/latest/fleet/config", &req, http.StatusOK, &res)
	assert.Len(t, res.Integrations.DigiCert.Value, 3)*/
}

func (s *integrationMDMTestSuite) TestDigiCertIntegrationWithHostPlatform() {
	t := s.T()
	ctx := context.Background()
	s.setSkipWorkerJobs(t)
	digiCertServer := createMockDigiCertServer(t)

	appConfig, err := s.ds.AppConfig(ctx)
	require.NoError(t, err)
	appConfig.MDM.MacOSSetup = fleet.MacOSSetup{
		EnableEndUserAuthentication: false,
	}
	err = s.ds.SaveAppConfig(ctx, appConfig)
	require.NoError(t, err)

	// Add DigiCert config
	ca := newMockDigicertCA(digiCertServer.server.URL, "my_CA")
	ca.APIToken = "api_token0"
	certPayload := createCertificateAuthorityRequest{
		CertificateAuthorityPayload: fleet.CertificateAuthorityPayload{
			DigiCert: &ca,
		},
	}
	res := createCertificateAuthorityResponse{}
	s.DoJSON("POST", "/api/latest/fleet/certificate_authorities", &certPayload, http.StatusOK, &res)

	// Create a host and then enroll to MDM.
	host, mdmDevice := createHostThenEnrollMDM(s.ds, s.server.URL, t)
	setupPusher(s, t, mdmDevice)
	// trigger a profile sync
	s.awaitTriggerProfileSchedule(t)
	profiles, err := s.ds.GetHostMDMAppleProfiles(ctx, host.UUID)
	require.NoError(t, err)
	require.GreaterOrEqual(t, len(profiles), 0)

	// Receive enrollment profiles (we are not checking/testing these here)
	for {
		cmd, err := mdmDevice.Idle()
		require.NoError(t, err)
		if cmd == nil {
			break
		}
		_, err = mdmDevice.Acknowledge(cmd.CommandUUID)
		require.NoError(t, err)
	}

	// ////////////////////////////////
	// Test DigiCert CA with HOST_PLATFORM Fleet variable
	caPlatform := fleet.DigiCertCA{
		Name:                          "PlatformVar",
		URL:                           digiCertServer.server.URL,
		APIToken:                      "api_token",
		ProfileID:                     "profile_id",
		CertificateCommonName:         "$FLEET_VAR_" + string(fleet.FleetVarHostPlatform) + "-device",
		CertificateUserPrincipalNames: []string{"${FLEET_VAR_" + string(fleet.FleetVarHostPlatform) + "}@example.com"},
		CertificateSeatID:             "seat_${FLEET_VAR_" + string(fleet.FleetVarHostPlatform) + "}",
	}
	certPayload = createCertificateAuthorityRequest{
		CertificateAuthorityPayload: fleet.CertificateAuthorityPayload{
			DigiCert: &caPlatform,
		},
	}
	resPlatform := createCertificateAuthorityResponse{}
	s.DoJSON("POST", "/api/latest/fleet/certificate_authorities", &certPayload, http.StatusOK, &resPlatform)

	profilePlatform := digiCertForTest("N5", "I5", "PlatformVar")
	s.Do("POST", "/api/latest/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
		{Name: "N5", Contents: profilePlatform},
	}}, http.StatusNoContent)
	profiles, err = s.ds.GetHostMDMAppleProfiles(ctx, host.UUID)
	require.NoError(t, err)
	require.GreaterOrEqual(t, len(profiles), 1)

	// trigger a profile sync
	s.awaitTriggerProfileSchedule(t)
	s.assertConfigProfilesByIdentifier(nil, "I5", true)

	// Check that status is pending (and not failed)
	getHostResp := getDeviceHostResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &getHostResp)
	require.NotNil(t, getHostResp.Host.MDM.Profiles)
	found := false
	for _, prof := range *getHostResp.Host.MDM.Profiles {
		if prof.Name == "N5" {
			found = true
			assert.Equal(t, fleet.MDMDeliveryPending, *prof.Status)
			assert.Equal(t, fleet.MDMOperationTypeInstall, prof.OperationType)
			assert.Empty(t, prof.Detail)
			continue
		}
	}
	assert.True(t, found)

	cmd, err := mdmDevice.Idle()
	require.NoError(t, err)
	require.NotNil(t, cmd, "Expecting PKCS12 certificate for platform variable")
	require.Equal(t, "InstallProfile", cmd.Command.RequestType)
	fullCmd := micromdm.CommandPayload{}
	require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd))

	cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID)
	require.NoError(t, err)
	assert.Nil(t, cmd)

	require.NotNil(t, fullCmd.Command)
	require.NotNil(t, fullCmd.Command.InstallProfile)
	rawProfile := fullCmd.Command.InstallProfile.Payload
	if !bytes.HasPrefix(rawProfile, []byte("<?xml")) {
		p7, err := pkcs7.Parse(rawProfile)
		require.NoError(t, err)
		require.NoError(t, p7.Verify())
		rawProfile = p7.Content
	}

	type pkcs12Profile struct {
		PayloadContent []map[string]interface{} `plist:"PayloadContent"`
	}

	pkcs12Prof := pkcs12Profile{}
	require.NoError(t, plist.Unmarshal(rawProfile, &pkcs12Prof))
	assert.Equal(t, "com.apple.security.pkcs12", pkcs12Prof.PayloadContent[0]["PayloadType"].(string))
	password := pkcs12Prof.PayloadContent[0]["Password"].(string)
	data := pkcs12Prof.PayloadContent[0]["PayloadContent"].([]byte)
	_, certificate, err := pkcs12.Decode(data, password)
	require.NoError(t, err)
	// darwin platform should be converted to "macos"
	assert.Equal(t, "macos-device", certificate.Subject.CommonName)

	// Check request to DigiCert for platform variable replacement
	digiCertServer.certReqMu.Lock()
	assert.Equal(t, caPlatform.ProfileID, digiCertServer.certReq.Profile["id"])
	assert.Equal(t, "seat_macos", digiCertServer.certReq.Seat["seat_id"])
	assert.Equal(t, "x509", digiCertServer.certReq.DeliveryFormat)
	assert.Equal(t, "macos-device", digiCertServer.certReq.Attributes["subject"].(map[string]interface{})["common_name"])

	// Need to convert UPNs to string slice since assert.Equal cannot compare string slice and interface slice
	upnsReceived := digiCertServer.certReq.Attributes["extensions"].(map[string]interface{})["san"].(map[string]interface{})["user_principal_names"].([]interface{})
	var stringSlice []string
	for _, item := range upnsReceived {
		if str, ok := item.(string); ok {
			stringSlice = append(stringSlice, str)
		}
	}
	assert.Equal(t, []string{"macos@example.com"}, stringSlice)
	digiCertServer.certReqMu.Unlock()

	// ////////////////////////////////
	// Modify the CN/UPN/Seat ID of the profile -- DigiCert verification should not happen
	digiCertServer.server.Close() // so that verify fails if attempted
}

//go:embed testdata/profiles/digicert.mobileconfig
var digiCertMobileconfig string

//go:embed testdata/profiles/digicert2.mobileconfig
var digiCert2Mobileconfig []byte

func digiCertForTest(name, identifier, caName string) []byte {
	return []byte(fmt.Sprintf(digiCertMobileconfig, caName, caName, name, identifier, uuid.New().String()))
}

//go:embed testdata/profiles/custom-scep.mobileconfig
var customSCEPMobileconfig string

//go:embed testdata/profiles/custom-scep2.mobileconfig
var customSCEPMobileconfig2 []byte

func customSCEPForTest(name, identifier, caName string) []byte {
	return []byte(fmt.Sprintf(customSCEPMobileconfig, caName, caName, name, identifier, uuid.New().String()))
}

type mockDigiCertServer struct {
	server    *httptest.Server
	certReq   certificateReq
	certReqMu *sync.Mutex
	notAfter  time.Time
}

type certificateReq struct {
	CSR            string                 `json:"csr"`
	Profile        map[string]string      `json:"profile"`
	Seat           map[string]string      `json:"seat"`
	DeliveryFormat string                 `json:"delivery_format"`
	Attributes     map[string]interface{} `json:"attributes"`
}

func createMockDigiCertServer(t *testing.T) *mockDigiCertServer {
	profileRegex := regexp.MustCompile(`^/mpki/api/v2/profile/([a-zA-Z0-9_-]+)$`)
	certRegex := regexp.MustCompile(`^/mpki/api/v1/certificate`)
	mockServer := &mockDigiCertServer{
		certReqMu: &sync.Mutex{},
		notAfter:  time.Now().Add(365 * 24 * time.Hour).UTC().Truncate(time.Second),
	}
	mockServer.server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		switch r.Method {
		case http.MethodGet:
			matches := profileRegex.FindStringSubmatch(r.URL.Path)
			if len(matches) != 2 {
				w.WriteHeader(http.StatusBadRequest)
				return
			}
			profileID := matches[1]

			resp := map[string]string{
				"id":     profileID,
				"name":   "Test CA",
				"status": "Active",
			}
			err := json.NewEncoder(w).Encode(resp)
			require.NoError(t, err)
		case http.MethodPost:
			if len(certRegex.FindStringSubmatch(r.URL.Path)) != 1 {
				w.WriteHeader(http.StatusBadRequest)
				return
			}
			mockServer.certReqMu.Lock()
			defer mockServer.certReqMu.Unlock()
			var req certificateReq
			err := json.NewDecoder(r.Body).Decode(&req)
			require.NoError(t, err)
			mockServer.certReq = req

			// Decode the PEM format CSR into DER
			block, _ := pem.Decode([]byte(req.CSR))
			require.NotNil(t, block, "failed to decode PEM block containing CSR")
			require.Equal(t, "CERTIFICATE REQUEST", block.Type, "unexpected PEM block type")

			// Parse the CSR
			csr, err := x509.ParseCertificateRequest(block.Bytes)
			require.NoError(t, err)
			require.NoError(t, csr.CheckSignature())

			// Setting CertificateCommonName to "Fail" allows us to test a failure getting a DigiCert certificate
			if csr.Subject.CommonName == "Fail" {
				w.WriteHeader(http.StatusBadRequest)
				type errorResponse struct {
					Errors interface{} `json:"errors"`
				}
				err = json.NewEncoder(w).Encode(errorResponse{
					Errors: []struct {
						Code    string `json:"code"`
						Message string `json:"message"`
					}{
						{
							Message: "Expected Fail",
						},
					},
				})
				require.NoError(t, err)
				return
			}

			// Generate a self-signed certificate based on the CSR
			serialNumber := big.NewInt(time.Now().Unix())
			certTemplate := &x509.Certificate{
				SerialNumber: serialNumber,
				Subject:      csr.Subject,
				NotBefore:    time.Now(),
				NotAfter:     mockServer.notAfter,
				KeyUsage:     x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
				ExtKeyUsage:  []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
			}

			privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
			require.NoError(t, err)

			certDER, err := x509.CreateCertificate(rand.Reader, certTemplate, certTemplate, &privateKey.PublicKey, privateKey)
			require.NoError(t, err)

			// Encode the certificate to PEM
			certPEM := new(bytes.Buffer)
			err = pem.Encode(certPEM, &pem.Block{Type: "CERTIFICATE", Bytes: certDER})
			require.NoError(t, err)

			w.WriteHeader(http.StatusCreated)
			resp := map[string]string{
				"serial_number":   serialNumber.String(),
				"delivery_format": "x509",
				"certificate":     certPEM.String(),
			}
			err = json.NewEncoder(w).Encode(resp)
			require.NoError(t, err)
		default:
			w.WriteHeader(http.StatusMethodNotAllowed)
		}
	}))
	t.Cleanup(mockServer.server.Close)
	return mockServer
}

func (s *integrationMDMTestSuite) TestCustomSCEPConfig() {
	t := s.T()
	ctx := context.Background()
	scepServer := scep_server.StartTestSCEPServer(t)
	scepServerURL := scepServer.URL + "/scep"

	// TODO(hca): Update test to use apply group CA endpoint?

	caBad := newMockCustomSCEPProxyCA(scepServer.URL, "ca")
	certPayload := createCertificateAuthorityRequest{
		CertificateAuthorityPayload: fleet.CertificateAuthorityPayload{
			CustomSCEPProxy: &caBad,
		},
	}
	res := createCertificateAuthorityResponse{}
	s.DoJSON("POST", "/api/latest/fleet/certificate_authorities", &certPayload, http.StatusBadRequest, &res)

	// Add 3 CustomSCEPProxy integrations
	ca0 := newMockCustomSCEPProxyCA(scepServerURL, "ca0")
	ca0.Challenge = "challenge0"
	ca1 := newMockCustomSCEPProxyCA(scepServerURL, "ca1")
	ca1.Challenge = "challenge1"
	ca2 := newMockCustomSCEPProxyCA(scepServerURL, "ca2")
	ca2.Challenge = "challenge2"
	certPayload.CertificateAuthorityPayload.CustomSCEPProxy = &ca0
	s.DoJSON("POST", "/api/latest/fleet/certificate_authorities", &certPayload, http.StatusOK, &res)
	certPayload.CertificateAuthorityPayload.CustomSCEPProxy = &ca1
	s.DoJSON("POST", "/api/latest/fleet/certificate_authorities", &certPayload, http.StatusOK, &res)
	certPayload.CertificateAuthorityPayload.CustomSCEPProxy = &ca2
	s.DoJSON("POST", "/api/latest/fleet/certificate_authorities", &certPayload, http.StatusOK, &res)

	listRes := listCertificateAuthoritiesResponse{}
	s.DoJSON("GET", "/api/latest/fleet/certificate_authorities", &listCertificateAuthoritiesRequest{}, http.StatusOK, &listRes)

	customScepProxies := []*fleet.CertificateAuthority{}
	customScepProxiesWithSecrets := []*fleet.CertificateAuthority{}
	for _, ca := range listRes.CertificateAuthorities {
		if ca.Type == string(fleet.CATypeCustomSCEPProxy) {
			getRes := getCertificateAuthorityResponse{}
			s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/certificate_authorities/%d", ca.ID), &getCertificateAuthorityRequest{}, http.StatusOK, &getRes)
			customScepProxies = append(customScepProxies, getRes.CertificateAuthority)

			caWithSecret, err := s.ds.GetCertificateAuthorityByID(ctx, ca.ID, true)
			require.NoError(t, err)
			customScepProxiesWithSecrets = append(customScepProxiesWithSecrets, caWithSecret)
		} else {
			t.Fatalf("found ca that is not a scep proxy")
		}
	}
	assert.Len(t, customScepProxies, 3)

	for _, ca := range customScepProxies {
		assert.Equal(t, fleet.MaskedPassword, *ca.Challenge)
	}

	require.Len(t, customScepProxiesWithSecrets, 3)
	assert.EqualValues(t, "challenge0", *customScepProxiesWithSecrets[0].Challenge)
	assert.EqualValues(t, "challenge1", *customScepProxiesWithSecrets[1].Challenge)
	assert.EqualValues(t, "challenge2", *customScepProxiesWithSecrets[2].Challenge)

	// Check 3 added activities are present
	var listActivities listActivitiesResponse
	s.DoJSON("GET", "/api/latest/fleet/activities", nil, http.StatusOK,
		&listActivities, "order_key", "a.id", "order_direction", "desc", "per_page", "10")
	require.True(t, len(listActivities.Activities) > 0)
	activity := fleet.ActivityAddedCustomSCEPProxy{}
	caNames := make([]string, 0, 3)
	for _, act := range listActivities.Activities {
		if act.Type == activity.ActivityName() {
			err := json.Unmarshal(*act.Details, &activity)
			require.NoError(t, err)
			caNames = append(caNames, activity.Name)
			if len(caNames) == 3 {
				break
			}
		}
	}
	slices.Sort(caNames)
	assert.EqualValues(t, caNames, []string{"ca0", "ca1", "ca2"})

	// TODO(HCA): Re enable and switch out with endpoints once update endpoint is implemented.
	/* // Add 1, modify 1, delete 1, keep 1 the same (CustomSCEPProxy integrations)
	ca1.Challenge = "challenge1-modified"
	ca3 := newMockCustomSCEPProxyCA(scepServerURL, "ca3")
	ca3.Challenge = "challenge3"
	appConfig = map[string]interface{}{
		"integrations": map[string]interface{}{
			"custom_scep_proxy": []fleet.CustomSCEPProxyCA{ca3, ca2, ca1},
		},
	}
	raw, err = json.Marshal(appConfig)
	require.NoError(t, err)
	req.RawMessage = raw
	res = appConfigResponse{}
	s.DoJSON("PATCH", "/api/latest/fleet/config", &req, http.StatusOK, &res)
	require.Len(t, res.Integrations.CustomSCEPProxy.Value, 3)
	assert.NotEqual(t, res.Integrations.CustomSCEPProxy.Value[0].Name, res.Integrations.CustomSCEPProxy.Value[1].Name)
	assert.NotEqual(t, res.Integrations.CustomSCEPProxy.Value[1].Name, res.Integrations.CustomSCEPProxy.Value[2].Name)
	for _, ca := range res.Integrations.CustomSCEPProxy.Value {
		switch ca.Name {
		case "ca1":
			assert.True(t, ca.Equals(&ca1))
		case "ca2":
			assert.True(t, ca.Equals(&ca2))
		case "ca3":
			assert.True(t, ca.Equals(&ca3))
		default:
			t.Fatalf("unexpected ca name: %s", ca.Name)
		}
		assert.Equal(t, ca.Challenge, fleet.MaskedPassword)
	}
	assets, err = s.ds.GetAllCAConfigAssetsByType(ctx, fleet.CAConfigCustomSCEPProxy)
	require.NoError(t, err)
	require.Len(t, assets, 3)
	assert.EqualValues(t, "challenge1-modified", assets["ca1"].Value)
	assert.EqualValues(t, "challenge2", assets["ca2"].Value)
	assert.EqualValues(t, "challenge3", assets["ca3"].Value)

	listActivities = listActivitiesResponse{}
	s.DoJSON("GET", "/api/latest/fleet/activities", nil, http.StatusOK,
		&listActivities, "order_key", "a.id", "order_direction", "desc", "per_page", "10")
	require.True(t, len(listActivities.Activities) > 0)
	activity = fleet.ActivityAddedCustomSCEPProxy{}
	editActivity := fleet.ActivityEditedCustomSCEPProxy{}
	delActivity := fleet.ActivityDeletedCustomSCEPProxy{}
	var numFound int
	for _, act := range listActivities.Activities {
		switch act.Type {
		case activity.ActivityName():
			err := json.Unmarshal(*act.Details, &activity)
			require.NoError(t, err)
			assert.Equal(t, activity.Name, ca3.Name)
			numFound++
		case editActivity.ActivityName():
			err := json.Unmarshal(*act.Details, &editActivity)
			require.NoError(t, err)
			assert.Equal(t, editActivity.Name, ca1.Name)
			numFound++
		case delActivity.ActivityName():
			err := json.Unmarshal(*act.Details, &delActivity)
			require.NoError(t, err)
			assert.Equal(t, delActivity.Name, ca0.Name)
			numFound++
		}
		if numFound == 3 {
			break
		}
	}
	assert.Equal(t, 3, numFound) */

	// Clear CustomSCEPProxy integrations
	for _, ca := range customScepProxies {
		s.DoRaw("DELETE", fmt.Sprintf("/api/latest/fleet/certificate_authorities/%d", ca.ID), nil, http.StatusNoContent)
	}

	// TODO(HCA): Re enable and switch out with endpoints once update endpoint is implemented.
	/*
		// Check that 3 deleted activities are present
		listActivities = listActivitiesResponse{}
		s.DoJSON("GET", "/api/latest/fleet/activities", nil, http.StatusOK,
			&listActivities, "order_key", "a.id", "order_direction", "desc", "per_page", "10")
		require.True(t, len(listActivities.Activities) > 0)
		delActivity = fleet.ActivityDeletedCustomSCEPProxy{}
		caNames = make([]string, 0, 3)
		for _, act := range listActivities.Activities {
			if act.Type == delActivity.ActivityName() {
				err := json.Unmarshal(*act.Details, &delActivity)
				require.NoError(t, err)
				caNames = append(caNames, delActivity.Name)
				if len(caNames) == 3 {
					break
				}
			}
		}
		slices.Sort(caNames)
		assert.EqualValues(t, caNames, []string{"ca1", "ca2", "ca3"})
	*/
}

func (s *integrationMDMTestSuite) TestCustomSCEPIntegration() {
	t := s.T()
	s.setSkipWorkerJobs(t)
	scepServer := scep_server.StartTestSCEPServer(t)
	scepServerURL := scepServer.URL + "/scep"

	// parseSCEPProfile returns the parsed SCEP profile along with the identifier from the profile URL
	parseSCEPProfile := func(t *testing.T, raw []byte, scepConfig fleet.CustomSCEPProxyCA) (SCEPProfileContent, string) {
		var fullCmd micromdm.CommandPayload
		require.NoError(t, plist.Unmarshal(raw, &fullCmd))
		require.NotNil(t, fullCmd.Command)
		require.NotNil(t, fullCmd.Command.InstallProfile)
		rawProfile := fullCmd.Command.InstallProfile.Payload
		if !bytes.HasPrefix(rawProfile, []byte("<?xml")) {
			p7, err := pkcs7.Parse(rawProfile)
			require.NoError(t, err)
			require.NoError(t, p7.Verify())
			rawProfile = p7.Content
		}
		var scepProfile SCEPProfileContent
		require.NoError(t, plist.Unmarshal(rawProfile, &scepProfile))

		require.Equal(t, "com.apple.security.scep", scepProfile.PayloadContent[0].PayloadType)
		require.Equal(t, scepConfig.Challenge, scepProfile.PayloadContent[0].PayloadContent.Challenge)
		expectBaseURL := s.server.URL + apple_mdm.SCEPProxyPath
		require.True(t, strings.HasPrefix(scepProfile.PayloadContent[0].PayloadContent.URL, expectBaseURL))
		identifier := strings.TrimPrefix(scepProfile.PayloadContent[0].PayloadContent.URL, expectBaseURL)

		return scepProfile, identifier
	}

	// parseIdentifier is a helper function to parse the identifier from the SCEP profile and check
	// components of the identifier against expected values. It returns the SCEP challenge.
	parseIdentifier := func(t *testing.T, identifier, wantHostUUID, wantProfUUID, wantSCEPName string) string {
		parts := strings.Split(identifier, url.PathEscape(","))
		require.Len(t, parts, 4)
		require.Equal(t, wantHostUUID, parts[0])
		require.Equal(t, wantProfUUID, parts[1])
		require.Equal(t, wantSCEPName, parts[2])
		gotChallenge := parts[3]
		require.NotEmpty(t, gotChallenge, "Challenge should not be empty in the identifier")
		return gotChallenge
	}

	// checkChallenge is a helper function to check if the challenge exists in the database.
	checkChallenge := func(t *testing.T, challenge string, expectFound bool) {
		mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
			var foundChallenge string
			stmt := "SELECT challenge FROM challenges where challenge = ?"
			err := sqlx.GetContext(context.Background(), q, &foundChallenge, stmt, challenge)
			switch {
			case errors.Is(err, sql.ErrNoRows):
				require.False(t, expectFound)
				require.Empty(t, foundChallenge)
				return nil
			default:
				require.NoError(t, err, "Failed to check challenge existence in the database")
				require.True(t, expectFound, fmt.Sprintf("found challenge %s, but expected not to find it", foundChallenge))
				require.NotEmpty(t, foundChallenge, "challenge should not be empty in the database")
				return nil
			}
		})
	}

	// checkProfileInstallStatus is a helper function to check the status of a profile in the host
	// response. It asserts that the profile with the given name exists, checks its status,
	// operation type, and detail. It returns the profile UUID for further checks.
	checkProfileInstallStatus := func(t *testing.T, hostResp getDeviceHostResponse, profileName string, wantStatus fleet.MDMDeliveryStatus, wantDetail string) string {
		var found bool
		require.NotNil(t, hostResp.Host.MDM.Profiles)
		var profileUUID string
		for _, prof := range *hostResp.Host.MDM.Profiles {
			if prof.Name == profileName {
				found = true
				require.Equal(t, wantStatus, *prof.Status)
				require.Equal(t, fleet.MDMOperationTypeInstall, prof.OperationType)
				require.Contains(t, prof.Detail, wantDetail)
				profileUUID = prof.ProfileUUID
				break
			}
		}
		require.True(t, found)
		return profileUUID
	}

	// verifySCEPProfile is a helper function to verify the SCEP profile in the host profiles.
	verifySCEPProfile := func(t *testing.T, host *fleet.Host, hostProf fleet.HostMacOSProfile, wantProfUUID string, wantCAName string) {
		hostProfs := map[string]*fleet.HostMacOSProfile{
			hostProf.Identifier: &hostProf,
		}
		require.NoError(t, apple_mdm.VerifyHostMDMProfiles(context.Background(), s.ds, host, hostProfs))
		prof, err := s.ds.GetAppleHostMDMCertificateProfile(context.Background(), host.UUID, wantProfUUID, wantCAName)
		require.NoError(t, err)
		require.NotNil(t, prof)
		require.Equal(t, wantCAName, prof.CAName)
		require.Equal(t, fleet.CAConfigCustomSCEPProxy, prof.Type)
		require.Equal(t, fleet.MDMDeliveryVerified, *prof.Status)
	}

	// Create a host and then enroll to MDM.
	host, mdmDevice := createHostThenEnrollMDM(s.ds, s.server.URL, t)
	setupPusher(s, t, mdmDevice)
	// trigger a profile sync
	s.awaitTriggerProfileSchedule(t)
	// Receive enrollment profiles (we are not checking/testing these here)
	for {
		cmd, err := mdmDevice.Idle()
		require.NoError(t, err)
		if cmd == nil {
			break
		}
		_, err = mdmDevice.Acknowledge(cmd.CommandUUID)
		require.NoError(t, err)
	}

	// /////////////////////////////////////////
	// Upload a profile without defining the CA
	resp := s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
		{Name: "N0", Contents: customSCEPForTest("N0", "I0", "scepName")},
	}}, http.StatusBadRequest)
	errMsg := extractServerErrorText(resp.Body)
	assert.Contains(t, errMsg, "FLEET_VAR_CUSTOM_SCEP_")
	assert.Contains(t, errMsg, "_scepName does not exist")

	// /////////////////////////////////////////
	// Add custom SCEP config
	ca0 := newMockCustomSCEPProxyCA(scepServerURL, "scepName")
	ca1 := newMockCustomSCEPProxyCA(scepServerURL, "scepName2")
	t.Logf("scepName2 challenge:%s", ca1.Challenge)
	certPayload := createCertificateAuthorityRequest{
		CertificateAuthorityPayload: fleet.CertificateAuthorityPayload{
			CustomSCEPProxy: &ca0,
		},
	}
	res1 := createCertificateAuthorityResponse{}
	s.DoJSON("POST", "/api/latest/fleet/certificate_authorities", &certPayload, http.StatusOK, &res1)
	certPayload.CertificateAuthorityPayload.CustomSCEPProxy = &ca1
	res2 := createCertificateAuthorityResponse{}
	s.DoJSON("POST", "/api/latest/fleet/certificate_authorities", &certPayload, http.StatusOK, &res2)

	// //////////////////////////////////////////////
	// Upload a profile with two valid SCEP payloads.
	resp = s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
		{Name: "CustomSCEP2", Contents: customSCEPMobileconfig2},
	}}, http.StatusBadRequest)
	errMsg = extractServerErrorText(resp.Body)
	assert.Contains(t, errMsg, fleet.MultipleSCEPPayloadsErrMsg)

	// /////////////////////////////////////////
	// Happy path: upload the profile containing a single valid SCEP config
	profileContents := customSCEPForTest("N0", "I0", "scepName")
	s.Do("POST", "/api/v1/fleet/mdm/profiles/batch", batchSetMDMProfilesRequest{Profiles: []fleet.MDMProfileBatchPayload{
		{Name: "N0", Contents: profileContents},
	}}, http.StatusNoContent)
	s.awaitTriggerProfileSchedule(t)
	getHostResp := getDeviceHostResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &getHostResp)
	profileUUID := checkProfileInstallStatus(t, getHostResp, "N0", fleet.MDMDeliveryPending, "")

	cmd, err := mdmDevice.Idle()
	require.NoError(t, err)
	require.NotNil(t, cmd, "Expecting SCEP profile")

	_, identifier := parseSCEPProfile(t, cmd.Raw, ca0)
	gotChallenge := parseIdentifier(t, identifier, host.UUID, profileUUID, "scepName")
	checkChallenge(t, gotChallenge, true)

	// /////////////////////////////////////
	// Test SCEP traffic being sent by host

	// Invalid profile identifier
	scepRes := s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+"invalid_identifier,p1234-uuid", nil, http.StatusBadRequest, nil, "operation", "GetCACaps")
	scepResErr := extractServerErrorText(scepRes.Body)
	require.Contains(t, scepResErr, "invalid profile UUID (only Apple, Windows, and Android")

	// Verify Windows profiles is allowed (with dummy values that will fail lookup)
	scepRes = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+"invalid_identifier,w1234-uuid", nil, http.StatusBadRequest, nil, "operation", "GetCACaps")
	scepResErr = extractServerErrorText(scepRes.Body)
	require.Contains(t, scepResErr, "unknown identifier in URL path") // Both invalid host and profile, but it allows it to lookup

	// GetCACaps
	scepRes = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+identifier, nil, http.StatusOK, nil, "operation", "GetCACaps")
	body, err := io.ReadAll(scepRes.Body)
	require.NoError(t, err)
	assert.Equal(t, scepserver.DefaultCACaps, string(body))
	// Check that the challenge is still in the database (only deleted after PKIOperation)
	checkChallenge(t, gotChallenge, true)

	// GetCACert
	scepRes = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+identifier, nil, http.StatusOK, nil, "operation", "GetCACert")
	body, err = io.ReadAll(scepRes.Body)
	require.NoError(t, err)
	certs, err := x509.ParseCertificates(body)
	require.NoError(t, err)
	assert.Len(t, certs, 1)
	// Check that the challenge is still in the database (only deleted after PKIOperation)
	checkChallenge(t, gotChallenge, true)

	// PKIOperation
	data, err := os.ReadFile("./testdata/PKCSReq.der")
	require.NoError(t, err)
	message := base64.StdEncoding.EncodeToString(data)

	// Note, a cert is not returned here because the message is not a fully valid SCEP request. However, building a valid SCEP request is a bit involved,
	// and this request is sufficient for testing our proxy functionality.
	scepRes = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+identifier, nil, http.StatusOK, nil, "operation",
		"PKIOperation", "message", message)
	body, err = io.ReadAll(scepRes.Body)
	require.NoError(t, err)
	pkiMessage, err := scep.ParsePKIMessage(body, scep.WithCACerts(certs))
	require.NoError(t, err)
	assert.Equal(t, scep.CertRep, pkiMessage.MessageType)
	// Check that the challenge is removed from the database after PKIOperation
	checkChallenge(t, gotChallenge, false)

	// Host acknowledges the profile and it is marked as verifying
	cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID)
	require.NoError(t, err)
	assert.Nil(t, cmd)

	getHostResp = getDeviceHostResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &getHostResp)
	profileUUIDTest := checkProfileInstallStatus(t, getHostResp, "N0", fleet.MDMDeliveryVerifying, "")
	require.Equal(t, profileUUID, profileUUIDTest, "Expected the same profile UUID after re-sending the SCEP profile")

	// Try again, it should fail because the profile is not pending
	_ = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+identifier, nil, http.StatusBadRequest, nil, "operation",
		"PKIOperation", "message", message)
	// Failed challenge doesn't resend the SCEP profile unless it is pending
	s.awaitTriggerProfileSchedule(t)
	cmd, err = mdmDevice.Idle()
	require.NoError(t, err)
	require.Nil(t, cmd, "Not expecting SCEP profile")

	// Mark the profile as pending so that it can be resent
	mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
		stmt := "UPDATE host_mdm_apple_profiles SET status = ? WHERE host_uuid = ? AND profile_uuid = ?"
		r, err := q.ExecContext(context.Background(), stmt, nil, host.UUID, profileUUID)
		require.NoError(t, err, "Failed to update profile status in the database")
		rowsAffected, _ := r.RowsAffected()
		require.Equal(t, int64(1), rowsAffected, "Expected to update 1 row for the profile status")
		return nil
	})

	// Try to do SCEP with deleted challenge, it will fail because challenge is no longer in the datastore
	// but a new challenge will be generated and the profile resent
	_ = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+identifier, nil, http.StatusBadRequest, nil, "operation",
		"PKIOperation", "message", message)
	// Check profile status, raw status will be nil (awaiting the reconcile job)
	mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
		var status string
		stmt := "SELECT coalesce(status, '') FROM host_mdm_apple_profiles WHERE host_uuid = ? AND profile_uuid = ?"
		err := sqlx.GetContext(context.Background(), q, &status, stmt, host.UUID, profileUUID)
		require.NoError(t, err)
		require.Empty(t, status, "Expected profile status to be nil after re-sending the SCEP profile")
		return nil
	})
	// Reconcile profiles, raw status becomes pending
	s.awaitTriggerProfileSchedule(t)
	mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
		var status string
		stmt := "SELECT coalesce(status, '') FROM host_mdm_apple_profiles WHERE host_uuid = ? AND profile_uuid = ?"
		err := sqlx.GetContext(context.Background(), q, &status, stmt, host.UUID, profileUUID)
		require.NoError(t, err)
		require.Equal(t, string(fleet.MDMDeliveryPending), status, "Expected profile status to be pending after re-sending the SCEP profile")
		return nil
	})

	// Device checks in again and should receive the SCEP profile
	cmd, err = mdmDevice.Idle()
	require.NoError(t, err)
	require.NotNil(t, cmd, "Expecting SCEP profile")
	// Status should still be pending because we haven't acknowledged the profile command yet
	mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
		var status string
		stmt := "SELECT coalesce(status, '')  FROM host_mdm_apple_profiles WHERE host_uuid = ? AND profile_uuid = ?"
		err := sqlx.GetContext(context.Background(), q, &status, stmt, host.UUID, profileUUID)
		require.NoError(t, err)
		require.Equal(t, string(fleet.MDMDeliveryPending), status, "Expected profile status to be pending after re-sending the SCEP profile")
		return nil
	})

	// Identifier should be different, and the challenge should be different
	_, identifier2 := parseSCEPProfile(t, cmd.Raw, ca0)
	require.NotEqual(t, identifier, identifier2, "Expected a different identifier after re-sending the SCEP profile")
	gotChallenge2 := parseIdentifier(t, identifier2, host.UUID, profileUUID, "scepName")
	require.NotEqual(t, gotChallenge, gotChallenge2, "Expected a new challenge after re-sending the SCEP profile")
	checkChallenge(t, gotChallenge, false) // old challenge deleted
	checkChallenge(t, gotChallenge2, true) // new challenge added

	// Check the host details
	getHostResp = getDeviceHostResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &getHostResp)
	profileUUID2 := checkProfileInstallStatus(t, getHostResp, "N0", fleet.MDMDeliveryPending, "")
	require.Equal(t, profileUUID, profileUUID2, "Expected the same profile UUID after re-sending the SCEP profile")

	// Expire the challenge so that the next PKIOperation fails
	mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
		stmt := "UPDATE challenges SET created_at = ? WHERE challenge = ?"
		res, err := q.ExecContext(context.Background(), stmt, time.Now().Add(-2*time.Hour), gotChallenge2)
		require.NoError(t, err, "Failed to expire the challenge in the database")
		rowsAffected, _ := res.RowsAffected()
		require.Equal(t, int64(1), rowsAffected, "Expected to update 1 row for the challenge")
		return nil
	})

	// Do SCEP again, it should fail because the challenge is expired but a new challenge should be
	// generated and the profile resent
	_ = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+identifier2, nil, http.StatusBadRequest, nil, "operation",
		"PKIOperation", "message", message)
	// Check profile status, raw status will be empty (awaiting the reconcile job)
	mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
		var status string
		stmt := "SELECT coalesce(status, '') FROM host_mdm_apple_profiles WHERE host_uuid = ? AND profile_uuid = ?"
		err := sqlx.GetContext(context.Background(), q, &status, stmt, host.UUID, profileUUID)
		require.NoError(t, err)
		require.Empty(t, status, "Expected profile status to be empty after re-sending the SCEP profile")
		return nil
	})

	// Check hosts details, derived status should still be pending
	getHostResp = getDeviceHostResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &getHostResp)
	profileUUID2 = checkProfileInstallStatus(t, getHostResp, "N0", fleet.MDMDeliveryPending, "")
	require.Equal(t, profileUUID, profileUUID2, "Expected the same profile UUID after re-sending the SCEP profile")

	// Reconcile profiles, raw status becomes pending
	s.awaitTriggerProfileSchedule(t)
	mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
		var status string
		stmt := "SELECT coalesce(status, '') FROM host_mdm_apple_profiles WHERE host_uuid = ? AND profile_uuid = ?"
		err := sqlx.GetContext(context.Background(), q, &status, stmt, host.UUID, profileUUID)
		require.NoError(t, err)
		require.Equal(t, string(fleet.MDMDeliveryPending), status, "Expected profile status to be pending after re-sending the SCEP profile")
		return nil
	})

	// Device checks in again and should receive the SCEP profile
	cmd, err = mdmDevice.Idle()
	require.NoError(t, err)
	require.NotNil(t, cmd, "Expecting SCEP profile")
	// Status should still be pending because we haven't acknowledged the profile command yet
	mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
		var status string
		stmt := "SELECT coalesce(status, '') FROM host_mdm_apple_profiles WHERE host_uuid = ? AND profile_uuid = ?"
		err := sqlx.GetContext(context.Background(), q, &status, stmt, host.UUID, profileUUID)
		require.NoError(t, err)
		require.Equal(t, string(fleet.MDMDeliveryPending), status, "Expected profile status to be pending after re-sending the SCEP profile")
		return nil
	})

	// Identifier should be different, and the challenge should be different
	_, identifier3 := parseSCEPProfile(t, cmd.Raw, ca0)
	require.NotEqual(t, identifier2, identifier3, "Expected a different identifier after re-sending the SCEP profile")
	gotChallenge3 := parseIdentifier(t, identifier3, host.UUID, profileUUID, "scepName")
	require.NotEqual(t, gotChallenge2, gotChallenge3, "Expected a new challenge after re-sending the SCEP profile")
	checkChallenge(t, gotChallenge, false)  // old challenge deleted
	checkChallenge(t, gotChallenge2, false) // old challenge deleted
	checkChallenge(t, gotChallenge3, true)  // new challenge added

	scepRes = s.DoRawWithHeaders("GET", apple_mdm.SCEPProxyPath+identifier3, nil, http.StatusOK, nil, "operation",
		"PKIOperation", "message", message)
	body, err = io.ReadAll(scepRes.Body)
	require.NoError(t, err)
	pkiMessage, err = scep.ParsePKIMessage(body, scep.WithCACerts(certs))
	require.NoError(t, err)
	assert.Equal(t, scep.CertRep, pkiMessage.MessageType)

	cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID)
	require.NoError(t, err)
	assert.Nil(t, cmd)
	getHostResp = getDeviceHostResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &getHostResp)
	profileUUID3 := checkProfileInstallStatus(t, getHostResp, "N0", fleet.MDMDeliveryVerifying, "")
	require.Equal(t, profileUUID2, profileUUID3, "Expected the same profile UUID after acknowledging the SCEP profile")

	verifySCEPProfile(t, host, fleet.HostMacOSProfile{
		Identifier:  "I0",
		DisplayName: "N0",
		InstallDate: time.Now(),
	}, profileUUID, "scepName")

	getHostResp = getDeviceHostResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &getHostResp)
	profileUUID4 := checkProfileInstallStatus(t, getHostResp, "N0", fleet.MDMDeliveryVerified, "")
	require.Equal(t, profileUUID3, profileUUID4, "Expected the same profile UUID after verifying the SCEP profile")

	// No more commands pending
	cmd, err = mdmDevice.Idle()
	require.NoError(t, err)
	require.Nil(t, cmd)

	// ////////////////////////////////////////////
	// Remove the CAs and try to re-send the profile
	s.DoRaw("DELETE", fmt.Sprintf("/api/latest/fleet/certificate_authorities/%d", res1.ID), nil, http.StatusNoContent)
	s.DoRaw("DELETE", fmt.Sprintf("/api/latest/fleet/certificate_authorities/%d", res2.ID), nil, http.StatusNoContent)
	_ = s.DoRaw("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/configuration_profiles/%s/resend", host.ID, profileUUID), nil, http.StatusAccepted)
	s.awaitTriggerProfileSchedule(t)
	getHostResp = getDeviceHostResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &getHostResp)
	checkProfileInstallStatus(t, getHostResp, "N0", fleet.MDMDeliveryFailed,
		"scepName certificate authority doesn't exist")
}

func (s *integrationMDMTestSuite) TestVPPAppsMDMFiltering() {
	t := s.T()

	ctx := context.Background()

	// Create hosts
	orbitHost := createOrbitEnrolledHost(t, "darwin", "nonmdm", s.ds)
	mdmHost, mdmClient := createHostThenEnrollMDM(s.ds, s.server.URL, t)
	_, _ = mdmHost, mdmClient

	test.CreateInsertGlobalVPPToken(t, s.ds)

	// Create team and add hosts to team
	var newTeamResp teamResponse
	s.DoJSON("POST", "/api/latest/fleet/teams", &createTeamRequest{TeamPayload: fleet.TeamPayload{Name: ptr.String("Team 1")}}, http.StatusOK, &newTeamResp)
	team := newTeamResp.Team

	s.Do("POST", "/api/latest/fleet/hosts/transfer", &addHostsToTeamRequest{HostIDs: []uint{orbitHost.ID, mdmHost.ID}, TeamID: &team.ID}, http.StatusOK)

	// Add an app so that we don't get a not found error
	_, err := s.ds.InsertVPPAppWithTeam(ctx, &fleet.VPPApp{
		Name:             "App " + t.Name(),
		BundleIdentifier: "bid_" + t.Name(),
		VPPAppTeam: fleet.VPPAppTeam{
			VPPAppID: fleet.VPPAppID{
				AdamID:   "adam_test_01",
				Platform: fleet.MacOSPlatform,
			},
		},
	}, &team.ID)
	require.NoError(t, err)

	resp := getHostSoftwareResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/software", orbitHost.ID), getHostSoftwareRequest{}, http.StatusOK, &resp)
	assert.Len(t, resp.Software, 0)

	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/software", mdmHost.ID), getHostSoftwareRequest{}, http.StatusOK, &resp)
	assert.Len(t, resp.Software, 1)
}

func (s *integrationMDMTestSuite) TestSetupExperience() {
	t := s.T()
	ds := s.ds
	ctx := context.Background()

	test.CreateInsertGlobalVPPToken(t, ds)
	user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true)

	team1, err := ds.NewTeam(ctx, &fleet.Team{Name: "team1"})
	require.NoError(t, err)

	tfr1, err := fleet.NewTempFileReader(strings.NewReader("hello"), t.TempDir)
	require.NoError(t, err)
	swInstallerPayload1 := fleet.UploadSoftwareInstallerPayload{
		InstallScript:     "hello",
		PreInstallQuery:   "SELECT 1",
		PostInstallScript: "world",
		UninstallScript:   "goodbye",
		InstallerFile:     tfr1,
		StorageID:         "storage1",
		Filename:          "file1",
		Title:             "file1",
		Version:           "1.0",
		Source:            "apps",
		UserID:            user1.ID,
		TeamID:            &team1.ID,
		Platform:          string(fleet.MacOSPlatform),
		ValidatedLabels:   &fleet.LabelIdentsWithScope{},
	}
	installerID1, titleID1, err := ds.MatchOrCreateSoftwareInstaller(ctx, &swInstallerPayload1)
	_ = installerID1
	require.NoError(t, err)

	app1 := &fleet.VPPApp{Name: "vpp_app_1", VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "1", Platform: fleet.MacOSPlatform}}, BundleIdentifier: "b1"}
	_, err = ds.InsertVPPAppWithTeam(ctx, app1, &team1.ID)
	require.NoError(t, err)

	var respListTitles listSoftwareTitlesResponse
	s.DoJSON(
		"GET", "/api/latest/fleet/software/titles",
		listSoftwareTitlesRequest{},
		http.StatusOK, &respListTitles,
		"team_id", fmt.Sprint(team1.ID),
	)
	require.Len(t, respListTitles.SoftwareTitles, 2)

	var respGetSetupExperience getSetupExperienceSoftwareResponse
	s.DoJSON("GET", "/api/latest/fleet/setup_experience/software", getSetupExperienceSoftwareRequest{}, http.StatusOK, &respGetSetupExperience, "team_id", fmt.Sprint(team1.ID))
	require.Len(t, respGetSetupExperience.SoftwareTitles, 2)

	var respPutSetupExperience putSetupExperienceSoftwareResponse
	s.DoJSON("PUT", "/api/latest/fleet/setup_experience/software", putSetupExperienceSoftwareRequest{TeamID: team1.ID, TitleIDs: []uint{respListTitles.SoftwareTitles[0].ID, respListTitles.SoftwareTitles[1].ID}}, http.StatusOK, &respPutSetupExperience)
	require.Nil(t, respPutSetupExperience.Error())

	s.lastActivityOfTypeMatches(fleet.ActivityEditedSetupExperienceSoftware{}.ActivityName(),
		fmt.Sprintf(`{"platform": "darwin", "team_id": %d, "team_name": "%s"}`, team1.ID, team1.Name), 0)

	respPutSetupExperience = putSetupExperienceSoftwareResponse{}
	s.DoJSON("PUT", "/api/latest/fleet/setup_experience/software", putSetupExperienceSoftwareRequest{
		Platform: "macos",
		TeamID:   team1.ID,
		TitleIDs: []uint{respListTitles.SoftwareTitles[0].ID, respListTitles.SoftwareTitles[1].ID},
	}, http.StatusOK, &respPutSetupExperience)
	require.Nil(t, respPutSetupExperience.Error())

	s.lastActivityOfTypeMatches(fleet.ActivityEditedSetupExperienceSoftware{}.ActivityName(),
		fmt.Sprintf(`{"platform": "darwin", "team_id": %d, "team_name": "%s"}`, team1.ID, team1.Name), 0)

	s.DoJSON("GET", "/api/latest/fleet/setup_experience/software", getSetupExperienceSoftwareRequest{}, http.StatusOK, &respGetSetupExperience, "team_id", fmt.Sprint(team1.ID))
	require.Len(t, respGetSetupExperience.SoftwareTitles, 2)

	desktopToken := uuid.New().String()
	mdmDevice := mdmtest.NewTestMDMClientAppleDesktopManual(s.server.URL, desktopToken)
	fleetHost, err := ds.NewHost(context.Background(), &fleet.Host{
		DetailUpdatedAt: time.Now(),
		LabelUpdatedAt:  time.Now(),
		PolicyUpdatedAt: time.Now(),
		SeenTime:        time.Now().Add(-1 * time.Minute),
		OsqueryHostID:   ptr.String(t.Name() + uuid.New().String()),
		NodeKey:         ptr.String(t.Name() + uuid.New().String()),
		Hostname:        fmt.Sprintf("%sfoo.local", t.Name()),
		Platform:        "darwin",
		HardwareModel:   "MacBookPro16,1",
		TeamID:          &team1.ID,

		UUID:           mdmDevice.UUID,
		HardwareSerial: mdmDevice.SerialNumber,
	})
	require.NoError(t, err)

	err = ds.SetOrUpdateDeviceAuthToken(context.Background(), fleetHost.ID, desktopToken)
	require.NoError(t, err)

	orbitNodeKey := setOrbitEnrollment(t, fleetHost, ds)

	err = mdmDevice.Enroll()
	require.NoError(t, err)

	var orbitRes getOrbitSetupExperienceStatusResponse
	s.DoJSON("POST", "/api/fleet/orbit/setup_experience/status", getOrbitSetupExperienceStatusRequest{OrbitNodeKey: orbitNodeKey}, http.StatusOK, &orbitRes)

	require.Len(t, orbitRes.Results.Software, 2)

	var vppFound, softwareFound bool
	for _, res := range orbitRes.Results.Software {

		if res.Name == "file1" {
			softwareFound = true
		}
		if res.Name == "vpp_app_1" {
			vppFound = true
		}
	}

	require.True(t, vppFound, "vpp app not found in status results")
	require.True(t, softwareFound, "software installer app not found in status results")

	awaitingConfig, err := s.ds.GetHostAwaitingConfiguration(ctx, fleetHost.UUID)
	require.NoError(t, err)
	require.True(t, awaitingConfig)

	updatePayload := &fleet.UpdateSoftwareInstallerPayload{
		TitleID:           titleID1,
		InstallerID:       installerID1,
		Filename:          swInstallerPayload1.Filename,
		InstallScript:     ptr.String("some new content"),
		PreInstallQuery:   &swInstallerPayload1.PreInstallQuery,
		PostInstallScript: &swInstallerPayload1.PostInstallScript,
		UninstallScript:   &swInstallerPayload1.UninstallScript,
		Version:           swInstallerPayload1.Version,
		SelfService:       ptr.Bool(true),
		UserID:            user1.ID,
		TeamID:            &team1.ID,
		ValidatedLabels:   &fleet.LabelIdentsWithScope{},
	}

	s.updateSoftwareInstaller(t, updatePayload, http.StatusOK, "")

	s.DoJSON("POST", "/api/fleet/orbit/setup_experience/status", getOrbitSetupExperienceStatusRequest{OrbitNodeKey: orbitNodeKey}, http.StatusOK, &orbitRes)

	require.Len(t, orbitRes.Results.Software, 2)

	vppFound = false
	softwareFound = false
	for _, res := range orbitRes.Results.Software {

		if res.Name == "file1" {
			softwareFound = true
			assert.Equal(t, fleet.SetupExperienceStatusFailure, res.Status)
		}
		if res.Name == "vpp_app_1" {
			vppFound = true
		}
	}

	require.True(t, vppFound, "vpp app not found in status results")
	require.True(t, softwareFound, "software installer app not found in status results")

	awaitingConfig, err = s.ds.GetHostAwaitingConfiguration(ctx, fleetHost.UUID)
	require.NoError(t, err)
	require.True(t, awaitingConfig)
}

func (s *integrationMDMTestSuite) TestWindowsMigrationEnabled() {
	t := s.T()

	var acResp appConfigResponse
	s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp)
	require.True(t, acResp.MDM.WindowsEnabledAndConfigured)
	require.False(t, acResp.MDM.WindowsMigrationEnabled)

	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
		"mdm": {
			"windows_migration_enabled": true
		}
	}`), http.StatusOK, &acResp)
	require.True(t, acResp.MDM.WindowsEnabledAndConfigured)
	require.True(t, acResp.MDM.WindowsMigrationEnabled)
	s.lastActivityMatches(fleet.ActivityTypeEnabledWindowsMDMMigration{}.ActivityName(), "", 0)

	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
		"mdm": {
			"windows_migration_enabled": false
		}
	}`), http.StatusOK, &acResp)
	require.True(t, acResp.MDM.WindowsEnabledAndConfigured)
	require.False(t, acResp.MDM.WindowsMigrationEnabled)
	s.lastActivityMatches(fleet.ActivityTypeDisabledWindowsMDMMigration{}.ActivityName(), "", 0)

	// set migrations back to true to see if they turn false when turning MDM off
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
		"mdm": {
			"windows_migration_enabled": true
		}
	}`), http.StatusOK, &acResp)
	require.True(t, acResp.MDM.WindowsEnabledAndConfigured)
	require.True(t, acResp.MDM.WindowsMigrationEnabled)
	lastEnabledID := s.lastActivityMatches(fleet.ActivityTypeEnabledWindowsMDMMigration{}.ActivityName(), "", 0)

	// not providing any mdm update should leave the current values
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
		"mdm": {}
	}`), http.StatusOK, &acResp)
	require.True(t, acResp.MDM.WindowsEnabledAndConfigured)
	require.True(t, acResp.MDM.WindowsMigrationEnabled)
	// no new activity was created
	s.lastActivityOfTypeMatches(fleet.ActivityTypeEnabledWindowsMDMMigration{}.ActivityName(), "", lastEnabledID)

	// set to true again does not generate a new activity, was already true
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
		"mdm": {
			"windows_migration_enabled": true
		}
	}`), http.StatusOK, &acResp)
	require.True(t, acResp.MDM.WindowsEnabledAndConfigured)
	require.True(t, acResp.MDM.WindowsMigrationEnabled)
	s.lastActivityOfTypeMatches(fleet.ActivityTypeEnabledWindowsMDMMigration{}.ActivityName(), "", lastEnabledID)

	res := s.Do("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
		"mdm": {
			"windows_enabled_and_configured": false,
			"windows_migration_enabled": true
		}
	}`), http.StatusUnprocessableEntity)
	errMsg := extractServerErrorText(res.Body)
	require.Contains(t, errMsg, "Windows MDM is not enabled")

	// turn off Windows MDM and try to enable migrations in a distinct call
	s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
		"mdm": {
			"windows_enabled_and_configured": false
		}
	}`), http.StatusOK, &acResp)
	require.False(t, acResp.MDM.WindowsEnabledAndConfigured)
	require.False(t, acResp.MDM.WindowsMigrationEnabled)

	res = s.Do("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
		"mdm": {
			"windows_migration_enabled": true
		}
	}`), http.StatusUnprocessableEntity)
	errMsg = extractServerErrorText(res.Body)
	require.Contains(t, errMsg, "Windows MDM is not enabled")
}

func (s *integrationMDMTestSuite) TestHostsCantTurnMDMOff() {
	t := s.T()
	winHost, _ := createWindowsHostThenEnrollMDM(s.ds, s.server.URL, t)
	r := s.Do("DELETE", fmt.Sprintf("/api/latest/fleet/hosts/%d/mdm", winHost.ID), nil, http.StatusBadRequest)
	require.Contains(t, extractServerErrorText(r.Body), fleet.CantTurnOffMDMForWindowsHostsMessage)
}

func (s *integrationMDMTestSuite) TestVPPPolicyAutomationLabelScopingRetrigger() {
	t := s.T()
	ctx := context.Background()
	batchURL := "/api/latest/fleet/software/app_store_apps/batch"

	// Set up VPP token
	orgName := "Fleet Device Management Inc."
	token := "mycooltoken"
	expTime := time.Now().Add(200 * time.Hour).UTC().Round(time.Second)
	expDate := expTime.Format(fleet.VPPTimeFormat)
	tokenJSON := fmt.Sprintf(`{"expDate":"%s","token":"%s","orgName":"%s"}`, expDate, token, orgName)
	t.Setenv("FLEET_DEV_VPP_URL", s.appleVPPConfigSrv.URL)
	var validToken uploadVPPTokenResponse
	s.uploadDataViaForm("/api/latest/fleet/vpp_tokens", "token", "token.vpptoken", []byte(base64.StdEncoding.EncodeToString([]byte(tokenJSON))), http.StatusAccepted, "", &validToken)

	// Get the token
	var resp getVPPTokensResponse
	s.DoJSON("GET", "/api/latest/fleet/vpp_tokens", &getVPPTokensRequest{}, http.StatusOK, &resp)
	require.NoError(t, resp.Err)

	// Create a team
	var newTeamResp teamResponse
	s.DoJSON("POST", "/api/latest/fleet/teams", &createTeamRequest{TeamPayload: fleet.TeamPayload{Name: ptr.String("Team 1")}}, http.StatusOK, &newTeamResp)
	team := newTeamResp.Team

	// Create a host and add to team
	host, _ := createHostThenEnrollMDM(s.ds, s.server.URL, t)
	nodeKey := setOrbitEnrollment(t, host, s.ds)
	host.OrbitNodeKey = &nodeKey
	s.Do("POST", "/api/latest/fleet/hosts/transfer", &addHostsToTeamRequest{HostIDs: []uint{host.ID}, TeamID: &team.ID}, http.StatusOK)

	s.appleVPPConfigSrvConfig.SerialNumbers = append(s.appleVPPConfigSrvConfig.SerialNumbers, host.HardwareSerial)

	// Associate team to the VPP token.
	var resPatchVPP patchVPPTokensTeamsResponse
	s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/vpp_tokens/%d/teams", resp.Tokens[0].ID), patchVPPTokensTeamsRequest{TeamIDs: []uint{team.ID}}, http.StatusOK, &resPatchVPP)

	// Create a few labels
	var newLabelResp createLabelResponse
	s.DoJSON("POST", "/api/v1/fleet/labels", fleet.LabelPayload{
		Name:  uuid.NewString(),
		Query: "SELECT 1",
	}, http.StatusOK, &newLabelResp)
	label1 := newLabelResp.Label

	newLabelResp = createLabelResponse{}
	s.DoJSON("POST", "/api/v1/fleet/labels", fleet.LabelPayload{
		Name:  uuid.NewString(),
		Query: "SELECT 2",
	}, http.StatusOK, &newLabelResp)
	label2 := newLabelResp.Label

	newLabelResp = createLabelResponse{}
	s.DoJSON("POST", "/api/v1/fleet/labels", fleet.LabelPayload{
		Name:  uuid.NewString(),
		Query: "SELECT 3",
	}, http.StatusOK, &newLabelResp)
	label3 := newLabelResp.Label

	// Sleep to guarantee that host label updated at timestamp is greater than the label creation timestamps
	time.Sleep(1 * time.Second)

	// Add label1 and label2 to the host
	host.LabelUpdatedAt = time.Now()
	err := s.ds.RecordLabelQueryExecutions(context.Background(), host, map[uint]*bool{label1.ID: ptr.Bool(true), label2.ID: ptr.Bool(true)}, host.LabelUpdatedAt, false)
	require.NoError(t, err)

	// add VPP app. Add label1 and label3 as "exclude any" labels.
	var appResp getAppStoreAppsResponse
	s.DoJSON("GET", "/api/latest/fleet/software/app_store_apps", &getAppStoreAppsRequest{}, http.StatusOK, &appResp, "team_id",
		fmt.Sprint(team.ID))
	require.NoError(t, appResp.Err)
	addedApp := appResp.AppStoreApps[0]
	var addedMacOSApp addAppStoreAppResponse
	s.DoJSON("POST", "/api/latest/fleet/software/app_store_apps", &addAppStoreAppRequest{
		TeamID:           &team.ID,
		Platform:         addedApp.Platform,
		AppStoreID:       addedApp.AdamID,
		SelfService:      true,
		LabelsExcludeAny: []string{label1.Name, label3.Name},
	}, http.StatusOK, &addedMacOSApp)

	// Get software title ID of the added VPP app
	listSWTitlesResp := listSoftwareTitlesResponse{}
	s.DoJSON(
		"GET", "/api/latest/fleet/software/titles",
		listSoftwareTitlesRequest{},
		http.StatusOK, &listSWTitlesResp,
		"query", addedApp.Name,
		"team_id", fmt.Sprintf("%d", team.ID),
	)
	require.Len(t, listSWTitlesResp.SoftwareTitles, 1)
	require.NotNil(t, listSWTitlesResp.SoftwareTitles[0].AppStoreApp)
	vppAppTitleID := listSWTitlesResp.SoftwareTitles[0].ID

	var vppAppDetail getSoftwareTitleResponse
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d", vppAppTitleID), nil, http.StatusOK, &vppAppDetail, "team_id", fmt.Sprintf("%d", team.ID))
	require.NotNil(t, vppAppDetail.SoftwareTitle)
	require.NotNil(t, vppAppDetail.SoftwareTitle.AppStoreApp)

	policy1, err := s.ds.NewTeamPolicy(ctx, team.ID, nil, fleet.PolicyPayload{
		Name:     "policy1",
		Query:    "SELECT 1;",
		Platform: "darwin",
	})
	require.NoError(t, err)

	mtplr := modifyTeamPolicyResponse{}
	s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/%d/policies/%d", team.ID, policy1.ID), modifyTeamPolicyRequest{
		ModifyPolicyPayload: fleet.ModifyPolicyPayload{
			SoftwareTitleID: optjson.Any[uint]{Set: true, Valid: true, Value: vppAppTitleID},
		},
	}, http.StatusOK, &mtplr)

	// Send back a failed result for the policy.
	distributedResp := submitDistributedQueryResultsResponse{}
	s.DoJSONWithoutAuth("POST", "/api/osquery/distributed/write", genDistributedReqWithPolicyResults(
		host,
		map[uint]*bool{
			policy1.ID: ptr.Bool(false),
		},
	), http.StatusOK, &distributedResp)
	err = s.ds.UpdateHostPolicyCounts(ctx)
	require.NoError(t, err)
	policy1, err = s.ds.Policy(ctx, policy1.ID)
	require.NoError(t, err)

	// Note: host has label1, label2, and at this point the app has exclude_any: label1, label3
	// Because the VPP app is out of scope, the policy doesn't have a status
	require.Equal(t, uint(0), policy1.PassingHostCount)
	require.Equal(t, uint(0), policy1.FailingHostCount)

	// Update the include any labels. The host has label2, so this means that the software
	// moved in scope.
	updateAppReq := &updateAppStoreAppRequest{TeamID: &team.ID, SelfService: ptr.Bool(false), LabelsIncludeAny: []string{label2.Name}}
	s.Do("PATCH", fmt.Sprintf("/api/latest/fleet/software/titles/%d/app_store_app", vppAppTitleID), updateAppReq, http.StatusOK)

	// Send back a failed result for the policy.
	distributedResp = submitDistributedQueryResultsResponse{}
	s.DoJSONWithoutAuth("POST", "/api/osquery/distributed/write", genDistributedReqWithPolicyResults(
		host,
		map[uint]*bool{
			policy1.ID: ptr.Bool(false),
		},
	), http.StatusOK, &distributedResp)
	err = s.ds.UpdateHostPolicyCounts(ctx)
	require.NoError(t, err)
	policy1, err = s.ds.Policy(ctx, policy1.ID)
	require.NoError(t, err)

	// Note: host has label1, label2, and at this point the app has include_any: label2
	// Because the VPP app is in scope, the policy is failing
	require.Equal(t, uint(0), policy1.PassingHostCount)
	require.Equal(t, uint(1), policy1.FailingHostCount)

	// Update to exclude_any: label 2. This moves the software out of scope. The policy is still failing.
	updateAppReq = &updateAppStoreAppRequest{TeamID: &team.ID, SelfService: ptr.Bool(false), LabelsExcludeAny: []string{label2.Name}}
	s.Do("PATCH", fmt.Sprintf("/api/latest/fleet/software/titles/%d/app_store_app", vppAppTitleID), updateAppReq, http.StatusOK)

	err = s.ds.UpdateHostPolicyCounts(ctx)
	require.NoError(t, err)
	policy1, err = s.ds.Policy(ctx, policy1.ID)
	require.NoError(t, err)
	require.Equal(t, uint(0), policy1.PassingHostCount)
	require.Equal(t, uint(1), policy1.FailingHostCount)

	// Update to exclude_any: label3. Host has label1, label2, so the app is in scope again and
	// status should clear.

	var batchAssociateResponse batchAssociateAppStoreAppsResponse
	s.DoJSON("POST", batchURL, batchAssociateAppStoreAppsRequest{Apps: []fleet.VPPBatchPayload{
		{AppStoreID: addedApp.AdamID, LabelsExcludeAny: []string{label3.Name}},
	}}, http.StatusOK, &batchAssociateResponse, "team_name", team.Name)
	require.Len(t, batchAssociateResponse.Apps, 1)

	err = s.ds.UpdateHostPolicyCounts(ctx)
	require.NoError(t, err)
	policy1, err = s.ds.Policy(ctx, policy1.ID)
	require.NoError(t, err)
	require.Equal(t, uint(0), policy1.PassingHostCount)
	require.Equal(t, uint(0), policy1.FailingHostCount)

	// Sending back another failed response
	s.DoJSONWithoutAuth("POST", "/api/osquery/distributed/write", genDistributedReqWithPolicyResults(
		host,
		map[uint]*bool{
			policy1.ID: ptr.Bool(false),
		},
	), http.StatusOK, &distributedResp)
	err = s.ds.UpdateHostPolicyCounts(ctx)
	require.NoError(t, err)
	policy1, err = s.ds.Policy(ctx, policy1.ID)
	require.NoError(t, err)
	// Because the app is in scope, the policy should be failing again.
	require.Equal(t, uint(0), policy1.PassingHostCount)
	require.Equal(t, uint(1), policy1.FailingHostCount)

	// Update to exclude_any: label 2. This moves the software out of scope. The policy is still failing.
	updateAppReq = &updateAppStoreAppRequest{TeamID: &team.ID, SelfService: ptr.Bool(false), LabelsExcludeAny: []string{label2.Name}}
	s.Do("PATCH", fmt.Sprintf("/api/latest/fleet/software/titles/%d/app_store_app", vppAppTitleID), updateAppReq, http.StatusOK)

	err = s.ds.UpdateHostPolicyCounts(ctx)
	require.NoError(t, err)
	policy1, err = s.ds.Policy(ctx, policy1.ID)
	require.NoError(t, err)
	require.Equal(t, uint(0), policy1.PassingHostCount)
	require.Equal(t, uint(1), policy1.FailingHostCount)

	// Update to exclude_any: label3. Host has label1, label2, so the app is in scope again and
	// status should clear.
	updateAppReq = &updateAppStoreAppRequest{TeamID: &team.ID, SelfService: ptr.Bool(false), LabelsExcludeAny: []string{label3.Name}}
	s.Do("PATCH", fmt.Sprintf("/api/latest/fleet/software/titles/%d/app_store_app", vppAppTitleID), updateAppReq, http.StatusOK)

	err = s.ds.UpdateHostPolicyCounts(ctx)
	require.NoError(t, err)
	policy1, err = s.ds.Policy(ctx, policy1.ID)
	require.NoError(t, err)
	require.Equal(t, uint(0), policy1.PassingHostCount)
	require.Equal(t, uint(0), policy1.FailingHostCount)

	// Sending back another failed response
	s.DoJSONWithoutAuth("POST", "/api/osquery/distributed/write", genDistributedReqWithPolicyResults(
		host,
		map[uint]*bool{
			policy1.ID: ptr.Bool(false),
		},
	), http.StatusOK, &distributedResp)
	err = s.ds.UpdateHostPolicyCounts(ctx)
	require.NoError(t, err)
	policy1, err = s.ds.Policy(ctx, policy1.ID)
	require.NoError(t, err)
	// Because the app is in scope, the policy should be failing again.
	require.Equal(t, uint(0), policy1.PassingHostCount)
	require.Equal(t, uint(1), policy1.FailingHostCount)
}

func (s *integrationMDMTestSuite) TestRefreshVPPAppVersions() {
	t := s.T()
	ctx := context.Background()

	// Reset the iTunes data to what it was before this test
	t.Cleanup(func() {
		s.appleITunesSrvData = map[string]string{
			// macos app
			"1": `{"bundleId": "a-1", "artworkUrl512": "https://example.com/images/1", "version": "1.0.0", "trackName": "App 1", "TrackID": 1}`,
			// macos, ios, ipados app
			"2": `{"bundleId": "b-2", "artworkUrl512": "https://example.com/images/2", "version": "2.0.0", "trackName": "App 2", "TrackID": 2, "supportedDevices": ["MacDesktop-MacDesktop", "iPhone5s-iPhone5s", "iPadAir-iPadAir"] }`,
			// ipados app
			"3": `{"bundleId": "c-3", "artworkUrl512": "https://example.com/images/3", "version": "3.0.0", "trackName": "App 3", "TrackID": 3, "supportedDevices": ["iPadAir-iPadAir"] }`,

			"4": `{"bundleId": "d-4", "artworkUrl512": "https://example.com/images/4", "version": "4.0.0", "trackName": "App 4", "TrackID": 4}`,

			// App with 0 licenses
			"5": `{"bundleId": "e-5", "artworkUrl512": "https://example.com/images/5", "version": "5.0.0", "trackName": "App 5", "TrackID": 5}`,

			// More macOS apps
			"6":  `{"bundleId": "f-6", "artworkUrl512": "https://example.com/images/6", "version": "6.0.0", "trackName": "App 6", "TrackID": 6}`,
			"7":  `{"bundleId": "g-7", "artworkUrl512": "https://example.com/images/7", "version": "7.0.0", "trackName": "App 7", "TrackID": 7}`,
			"8":  `{"bundleId": "h-8", "artworkUrl512": "https://example.com/images/8", "version": "8.0.0", "trackName": "App 8", "TrackID": 8}`,
			"9":  `{"bundleId": "i-9", "artworkUrl512": "https://example.com/images/9", "version": "9.0.0", "trackName": "App 9", "TrackID": 9}`,
			"10": `{"bundleId": "j-10", "artworkUrl512": "https://example.com/images/10", "version": "10.0.0", "trackName": "App 10", "TrackID": 10}`,
		}
	})

	// Set up 3 apps - macOS, iOS, and iPadOS
	s.appleITunesSrvData = map[string]string{
		"1": `{"bundleId": "a-1", "artworkUrl512": "https://example.com/images/1", "version": "1.0.0", "trackName": "App 1", "TrackID": 1}`,
		"2": `{"bundleId": "d-2", "artworkUrl512": "https://example.com/images/2", "version": "2.0.0", "trackName": "App 2", "TrackID": 2, "supportedDevices": ["iPhone5s-iPhone5s"] }`,
		"3": `{"bundleId": "b-3", "artworkUrl512": "https://example.com/images/3", "version": "3.0.0", "trackName": "App 3", "TrackID": 3, "supportedDevices": ["iPadAir-iPadAir"] }`,
	}

	var newTeamResp teamResponse
	s.DoJSON("POST", "/api/latest/fleet/teams", &createTeamRequest{TeamPayload: fleet.TeamPayload{Name: ptr.String("Team 1" + t.Name())}}, http.StatusOK, &newTeamResp)
	team := newTeamResp.Team

	// Set up VPP token
	orgName := "Fleet Device Management Inc."
	token := "mycooltoken"
	expTime := time.Now().Add(200 * time.Hour).UTC().Round(time.Second)
	expDate := expTime.Format(fleet.VPPTimeFormat)
	tokenJSON := fmt.Sprintf(`{"expDate":"%s","token":"%s","orgName":"%s"}`, expDate, token, orgName)
	t.Setenv("FLEET_DEV_VPP_URL", s.appleVPPConfigSrv.URL)
	var validToken uploadVPPTokenResponse
	s.uploadDataViaForm("/api/latest/fleet/vpp_tokens", "token", "token.vpptoken", []byte(base64.StdEncoding.EncodeToString([]byte(tokenJSON))), http.StatusAccepted, "", &validToken)

	// Get the token
	var resp getVPPTokensResponse
	s.DoJSON("GET", "/api/latest/fleet/vpp_tokens", &getVPPTokensRequest{}, http.StatusOK, &resp)
	require.NoError(t, resp.Err)

	// Associate team to the VPP token.
	var resPatchVPP patchVPPTokensTeamsResponse
	s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/vpp_tokens/%d/teams", resp.Tokens[0].ID), patchVPPTokensTeamsRequest{TeamIDs: []uint{team.ID}}, http.StatusOK, &resPatchVPP)

	// No VPP apps added yet, so this is a no-op
	err := vpp.RefreshVersions(ctx, s.ds)
	require.NoError(t, err)

	var appResp getAppStoreAppsResponse
	s.DoJSON("GET", "/api/latest/fleet/software/app_store_apps", &getAppStoreAppsRequest{}, http.StatusOK, &appResp, "team_id",
		fmt.Sprint(team.ID))
	require.NoError(t, appResp.Err)
	app1 := appResp.AppStoreApps[0]
	app2 := appResp.AppStoreApps[1]
	app3 := appResp.AppStoreApps[2]

	// Add 3 apps: 2 will have version changes, the 3rd will not
	var addAppResp addAppStoreAppResponse
	s.DoJSON("POST", "/api/latest/fleet/software/app_store_apps", &addAppStoreAppRequest{
		TeamID:     &team.ID,
		Platform:   app1.Platform,
		AppStoreID: app1.AdamID,
	}, http.StatusOK, &addAppResp)

	s.DoJSON("POST", "/api/latest/fleet/software/app_store_apps", &addAppStoreAppRequest{
		TeamID:     &team.ID,
		Platform:   app2.Platform,
		AppStoreID: app2.AdamID,
	}, http.StatusOK, &addAppResp)

	s.DoJSON("POST", "/api/latest/fleet/software/app_store_apps", &addAppStoreAppRequest{
		TeamID:     &team.ID,
		Platform:   app3.Platform,
		AppStoreID: app3.AdamID,
	}, http.StatusOK, &addAppResp)

	// Check versions before refresh
	var listSWTitlesResp listSoftwareTitlesResponse
	s.DoJSON("GET", "/api/latest/fleet/software/titles", nil, http.StatusOK, &listSWTitlesResp, "team_id", fmt.Sprintf("%d", team.ID), "query", app1.Name)
	require.Len(t, listSWTitlesResp.SoftwareTitles, 1)
	require.NotNil(t, listSWTitlesResp.SoftwareTitles[0].AppStoreApp)
	require.Equal(t, "1.0.0", listSWTitlesResp.SoftwareTitles[0].AppStoreApp.Version)

	s.DoJSON("GET", "/api/latest/fleet/software/titles", nil, http.StatusOK, &listSWTitlesResp, "team_id", fmt.Sprintf("%d", team.ID), "query", app2.Name)
	require.Len(t, listSWTitlesResp.SoftwareTitles, 1)
	require.NotNil(t, listSWTitlesResp.SoftwareTitles[0].AppStoreApp)
	require.Equal(t, "2.0.0", listSWTitlesResp.SoftwareTitles[0].AppStoreApp.Version)

	s.DoJSON("GET", "/api/latest/fleet/software/titles", nil, http.StatusOK, &listSWTitlesResp, "team_id", fmt.Sprintf("%d", team.ID), "query", app3.Name)
	require.Len(t, listSWTitlesResp.SoftwareTitles, 1)
	require.NotNil(t, listSWTitlesResp.SoftwareTitles[0].AppStoreApp)
	require.Equal(t, "3.0.0", listSWTitlesResp.SoftwareTitles[0].AppStoreApp.Version)

	// "update" the versions
	s.appleITunesSrvData["1"] = `{"bundleId": "a-1", "artworkUrl512": "https://example.com/images/1", "version": "9.9.9", "trackName": "App 1", "TrackID": 1}`
	s.appleITunesSrvData["2"] = `{"bundleId": "b-2", "artworkUrl512": "https://example.com/images/2", "version": "10.10.10", "trackName": "App 2", "TrackID": 2,
				"supportedDevices": ["MacDesktop-MacDesktop", "iPhone5s-iPhone5s", "iPadAir-iPadAir"] }`

	err = vpp.RefreshVersions(ctx, s.ds)
	require.NoError(t, err)

	// 1 and 2 should be updated
	s.DoJSON("GET", "/api/latest/fleet/software/titles", nil, http.StatusOK, &listSWTitlesResp, "team_id", fmt.Sprintf("%d", team.ID), "query", app1.Name)
	require.Len(t, listSWTitlesResp.SoftwareTitles, 1)
	require.NotNil(t, listSWTitlesResp.SoftwareTitles[0].AppStoreApp)
	require.Equal(t, "9.9.9", listSWTitlesResp.SoftwareTitles[0].AppStoreApp.Version)

	s.DoJSON("GET", "/api/latest/fleet/software/titles", nil, http.StatusOK, &listSWTitlesResp, "team_id", fmt.Sprintf("%d", team.ID), "query", app2.Name)
	require.Len(t, listSWTitlesResp.SoftwareTitles, 1)
	require.NotNil(t, listSWTitlesResp.SoftwareTitles[0].AppStoreApp)
	require.Equal(t, "10.10.10", listSWTitlesResp.SoftwareTitles[0].AppStoreApp.Version)

	// No change
	s.DoJSON("GET", "/api/latest/fleet/software/titles", nil, http.StatusOK, &listSWTitlesResp, "team_id", fmt.Sprintf("%d", team.ID), "query", app3.Name)
	require.Len(t, listSWTitlesResp.SoftwareTitles, 1)
	require.NotNil(t, listSWTitlesResp.SoftwareTitles[0].AppStoreApp)
	require.Equal(t, "3.0.0", listSWTitlesResp.SoftwareTitles[0].AppStoreApp.Version)

	// Refresh again. There are no version changes this time, so this is a no-op.
	err = vpp.RefreshVersions(ctx, s.ds)
	require.NoError(t, err)
}

func (s *integrationMDMTestSuite) TestUpcomingActivitiesTurnMDMOff() {
	t := s.T()
	s.setSkipWorkerJobs(t)

	// Set up VPP token
	orgName := "Fleet Device Management Inc."
	token := "validtoken"
	expTime := time.Now().Add(200 * time.Hour).UTC().Round(time.Second)
	expDate := expTime.Format(fleet.VPPTimeFormat)
	tokenJSON := fmt.Sprintf(`{"expDate":"%s","token":"%s","orgName":"%s"}`, expDate, token, orgName)
	t.Setenv("FLEET_DEV_VPP_URL", s.appleVPPConfigSrv.URL)
	var validToken uploadVPPTokenResponse
	s.uploadDataViaForm("/api/latest/fleet/vpp_tokens", "token", "token.vpptoken", []byte(base64.StdEncoding.EncodeToString([]byte(tokenJSON))), http.StatusAccepted, "", &validToken)

	// Get the token
	var resp getVPPTokensResponse
	s.DoJSON("GET", "/api/latest/fleet/vpp_tokens", &getVPPTokensRequest{}, http.StatusOK, &resp)
	require.NoError(t, resp.Err)

	// Create a couple of hosts
	mdmHost, _ := createHostThenEnrollMDM(s.ds, s.server.URL, t)
	key := setOrbitEnrollment(t, mdmHost, s.ds)
	mdmHost.OrbitNodeKey = &key
	mdmHost2, _ := createHostThenEnrollMDM(s.ds, s.server.URL, t)
	key2 := setOrbitEnrollment(t, mdmHost2, s.ds)
	mdmHost2.OrbitNodeKey = &key2

	// Add serial number to our fake Apple server
	s.appleVPPConfigSrvConfig.SerialNumbers = append(s.appleVPPConfigSrvConfig.SerialNumbers, mdmHost.HardwareSerial, mdmHost2.HardwareSerial)

	// Associate team to the VPP token.
	var resPatchVPP patchVPPTokensTeamsResponse
	s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/vpp_tokens/%d/teams", resp.Tokens[0].ID), patchVPPTokensTeamsRequest{TeamIDs: []uint{}}, http.StatusOK, &resPatchVPP)

	// add VPP app
	var appResp getAppStoreAppsResponse
	s.DoJSON("GET", "/api/latest/fleet/software/app_store_apps", nil, http.StatusOK, &appResp, "team_id", "0")
	require.NoError(t, appResp.Err)
	require.True(t, len(appResp.AppStoreApps) > 0)
	addedApp := appResp.AppStoreApps[0]

	var addedMacOSApp addAppStoreAppResponse
	s.DoJSON("POST", "/api/latest/fleet/software/app_store_apps", &addAppStoreAppRequest{
		TeamID:     nil,
		Platform:   addedApp.Platform,
		AppStoreID: addedApp.AdamID,
	}, http.StatusOK, &addedMacOSApp)

	// Get software title ID of the added VPP app
	listSWTitlesResp := listSoftwareTitlesResponse{}
	s.DoJSON("GET", "/api/latest/fleet/software/titles", listSoftwareTitlesRequest{},
		http.StatusOK, &listSWTitlesResp, "query", addedApp.Name, "team_id", "0")
	require.Len(t, listSWTitlesResp.SoftwareTitles, 1)
	require.NotNil(t, listSWTitlesResp.SoftwareTitles[0].AppStoreApp)
	vppAppTitleID := listSWTitlesResp.SoftwareTitles[0].ID

	// make the VPP app install pending on both hosts
	s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/install", mdmHost.ID, vppAppTitleID), &installSoftwareRequest{}, http.StatusAccepted)
	s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/install", mdmHost2.ID, vppAppTitleID), &installSoftwareRequest{}, http.StatusAccepted)

	// also enqueue a script execution request on host 1
	var runResp runScriptResponse
	s.DoJSON("POST", "/api/latest/fleet/scripts/run", fleet.HostScriptRequestPayload{HostID: mdmHost.ID, ScriptContents: "echo"}, http.StatusAccepted, &runResp)

	// host 1 has 2 upcoming activities, the vpp app and the script
	var hostActivitiesResp listHostUpcomingActivitiesResponse
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/activities/upcoming", mdmHost.ID),
		nil, http.StatusOK, &hostActivitiesResp)
	require.Len(t, hostActivitiesResp.Activities, 2)
	require.Equal(t, fleet.ActivityInstalledAppStoreApp{}.ActivityName(), hostActivitiesResp.Activities[0].Type)
	require.Equal(t, fleet.ActivityTypeRanScript{}.ActivityName(), hostActivitiesResp.Activities[1].Type)
	scriptExecID := hostActivitiesResp.Activities[1].UUID

	// host 2 has the VPP app
	hostActivitiesResp = listHostUpcomingActivitiesResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/activities/upcoming", mdmHost2.ID),
		nil, http.StatusOK, &hostActivitiesResp)
	require.Len(t, hostActivitiesResp.Activities, 1)
	require.Equal(t, fleet.ActivityInstalledAppStoreApp{}.ActivityName(), hostActivitiesResp.Activities[0].Type)
	host2VppAppExecID := hostActivitiesResp.Activities[0].UUID

	// turn off MDM for host 1
	s.Do("DELETE", fmt.Sprintf("/api/latest/fleet/hosts/%d/mdm", mdmHost.ID), nil, http.StatusNoContent)

	// confirm that this host's MDM is off
	res := s.Do("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/mdm", mdmHost.ID), nil, http.StatusOK)
	b, err := io.ReadAll(res.Body)
	require.NoError(t, err)
	var m map[string]any
	err = json.Unmarshal(b, &m)
	require.NoError(t, err)
	require.Equal(t, "Off", m["enrollment_status"])

	// host 1 has only the script remaining
	hostActivitiesResp = listHostUpcomingActivitiesResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/activities/upcoming", mdmHost.ID),
		nil, http.StatusOK, &hostActivitiesResp)
	require.Len(t, hostActivitiesResp.Activities, 1)
	require.Equal(t, fleet.ActivityTypeRanScript{}.ActivityName(), hostActivitiesResp.Activities[0].Type)
	require.Equal(t, scriptExecID, hostActivitiesResp.Activities[0].UUID)

	// host 2 still has the VPP app
	hostActivitiesResp = listHostUpcomingActivitiesResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/activities/upcoming", mdmHost2.ID),
		nil, http.StatusOK, &hostActivitiesResp)
	require.Len(t, hostActivitiesResp.Activities, 1)
	require.Equal(t, fleet.ActivityInstalledAppStoreApp{}.ActivityName(), hostActivitiesResp.Activities[0].Type)
	require.Equal(t, host2VppAppExecID, hostActivitiesResp.Activities[0].UUID)

	// save a script result for host 1, as the script should've been activated
	// when MDM was turned off
	var orbitPostScriptResp orbitPostScriptResultResponse
	s.DoJSON("POST", "/api/fleet/orbit/scripts/result",
		json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q, "execution_id": %q, "exit_code": 0, "output": "ok"}`, *mdmHost.OrbitNodeKey, scriptExecID)),
		http.StatusOK, &orbitPostScriptResp)

	// host 1 has no remaining upcoming activity
	hostActivitiesResp = listHostUpcomingActivitiesResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/activities/upcoming", mdmHost.ID),
		nil, http.StatusOK, &hostActivitiesResp)
	require.Len(t, hostActivitiesResp.Activities, 0)
}

// Test for #24265
func (s *integrationMDMTestSuite) TestNonMDWindowsHostsIgnoredInDiskEncryptionStats() {
	t := s.T()
	ctx := context.Background()
	s.setSkipWorkerJobs(t)

	// get the All hosts label ID
	ls, err := s.ds.LabelIDsByName(ctx, []string{"All Hosts"})
	require.NoError(t, err)
	require.Len(t, ls, 1)
	allHostsLblID := ls["All Hosts"]

	// create a couple Windows non-MDM-enrolled hosts
	winHost1 := createOrbitEnrolledHost(t, "windows", "h1", s.ds)
	winHost2 := createOrbitEnrolledHost(t, "windows", "h2", s.ds)
	err = s.ds.SetOrUpdateMDMData(ctx, winHost1.ID, false, false, "", false, "", "", false)
	require.NoError(t, err)
	err = s.ds.SetOrUpdateMDMData(ctx, winHost2.ID, false, false, "", false, "", "", false)
	require.NoError(t, err)
	err = s.ds.AddLabelsToHost(ctx, winHost1.ID, []uint{allHostsLblID})
	require.NoError(t, err)
	err = s.ds.AddLabelsToHost(ctx, winHost2.ID, []uint{allHostsLblID})
	require.NoError(t, err)

	// first a quick stats check without disk encryption
	s.Do("POST", "/api/latest/fleet/disk_encryption", updateDiskEncryptionRequest{EnableDiskEncryption: false}, http.StatusNoContent)
	acResp := appConfigResponse{}
	s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp)
	assert.False(t, acResp.MDM.EnableDiskEncryption.Value)

	s.checkMDMProfilesSummaries(t, nil, fleet.MDMProfilesSummary{}, nil)
	s.checkMDMDiskEncryptionSummaries(t, nil, fleet.MDMDiskEncryptionSummary{}, false)

	// enable disk encryption
	s.Do("POST", "/api/latest/fleet/disk_encryption", updateDiskEncryptionRequest{EnableDiskEncryption: true}, http.StatusNoContent)
	acResp = appConfigResponse{}
	s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp)
	assert.True(t, acResp.MDM.EnableDiskEncryption.Value)

	// all profiles counts are expected to be 0 (no host is MDM-enrolled in Fleet)
	s.checkMDMProfilesSummaries(t, nil, fleet.MDMProfilesSummary{}, nil)

	// filter hosts with any OS Settings status, should have none
	statuses := []fleet.OSSettingsStatus{
		fleet.OSSettingsVerified, fleet.OSSettingsVerifying,
		fleet.OSSettingsPending, fleet.OSSettingsFailed,
	}
	for _, status := range statuses {
		s.checkListHostsFilter(t, allHostsLblID, "os_settings", string(status))
	}

	// all disk encryption counts are expected to be 0 (no host is MDM-enrolled in Fleet)
	s.checkMDMDiskEncryptionSummaries(t, nil, fleet.MDMDiskEncryptionSummary{}, false)

	// filter hosts with any Disk Encryption status, should have none
	diskStatuses := []fleet.DiskEncryptionStatus{
		fleet.DiskEncryptionVerified, fleet.DiskEncryptionVerifying,
		fleet.DiskEncryptionActionRequired, fleet.DiskEncryptionEnforcing,
		fleet.DiskEncryptionFailed, fleet.DiskEncryptionRemovingEnforcement,
	}
	for _, status := range diskStatuses {
		s.checkListHostsFilter(t, allHostsLblID, "os_settings_disk_encryption", string(status))
	}

	// enroll a Windows host in Fleet MDM
	enrollWindowsHostInMDMViaOrbit(t, winHost1, s.ds, s.server.URL)

	// stats should now count this host
	s.checkMDMProfilesSummaries(t, nil, fleet.MDMProfilesSummary{Pending: 1}, nil)
	s.checkMDMDiskEncryptionSummaries(t, nil, fleet.MDMDiskEncryptionSummary{Enforcing: fleet.MDMPlatformsCounts{Windows: 1}}, false)

	// filters should return this host
	s.checkListHostsFilter(t, allHostsLblID, "os_settings", string(fleet.OSSettingsPending), winHost1.ID)
	s.checkListHostsFilter(t, allHostsLblID, "os_settings_disk_encryption", string(fleet.DiskEncryptionEnforcing), winHost1.ID)

	// enroll the other Windows host in a third-party MDM
	err = s.ds.SetOrUpdateMDMData(ctx, winHost2.ID, false, true, "https://simplemdm.com", true, fleet.WellKnownMDMSimpleMDM, "", false)
	require.NoError(t, err)

	// stats should NOT count winHost2 (not in Fleet MDM)
	s.checkMDMProfilesSummaries(t, nil, fleet.MDMProfilesSummary{Pending: 1}, nil)
	s.checkMDMDiskEncryptionSummaries(t, nil, fleet.MDMDiskEncryptionSummary{Enforcing: fleet.MDMPlatformsCounts{Windows: 1}}, false)

	// filters should NOT return this host
	s.checkListHostsFilter(t, allHostsLblID, "os_settings", string(fleet.OSSettingsPending), winHost1.ID)
	s.checkListHostsFilter(t, allHostsLblID, "os_settings_disk_encryption", string(fleet.DiskEncryptionEnforcing), winHost1.ID)
}

// Test for #24266
func (s *integrationMDMTestSuite) TestLinuxHostsIgnoredInOSSettingsStats() {
	t := s.T()
	ctx := context.Background()
	s.setSkipWorkerJobs(t)

	// get the All hosts label ID
	ls, err := s.ds.LabelIDsByName(ctx, []string{"All Hosts"})
	require.NoError(t, err)
	require.Len(t, ls, 1)
	allHostsLblID := ls["All Hosts"]

	// create a couple Linux hosts that would support disk encryption if it was enabled
	linuxHost1 := createOrbitEnrolledHost(t, "ubuntu", "h1", s.ds)
	linuxHost2 := createOrbitEnrolledHost(t, "rhel", "h2", s.ds)
	linuxHost2.OSVersion = "Fedora 41"
	err = s.ds.UpdateHost(ctx, linuxHost2)
	require.NoError(t, err)
	linuxHost3 := createOrbitEnrolledHost(t, "arch", "h3", s.ds)

	err = s.ds.AddLabelsToHost(ctx, linuxHost1.ID, []uint{allHostsLblID})
	require.NoError(t, err)
	err = s.ds.AddLabelsToHost(ctx, linuxHost2.ID, []uint{allHostsLblID})
	require.NoError(t, err)
	err = s.ds.AddLabelsToHost(ctx, linuxHost3.ID, []uint{allHostsLblID})
	require.NoError(t, err)

	// ensure disk encryption is disabled
	s.Do("POST", "/api/latest/fleet/disk_encryption", updateDiskEncryptionRequest{EnableDiskEncryption: false}, http.StatusNoContent)
	acResp := appConfigResponse{}
	s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp)
	assert.False(t, acResp.MDM.EnableDiskEncryption.Value)

	// all profiles counts are expected to be 0
	s.checkMDMProfilesSummaries(t, nil, fleet.MDMProfilesSummary{}, nil)

	// filter hosts with any OS Settings status, should have none
	statuses := []fleet.OSSettingsStatus{
		fleet.OSSettingsVerified, fleet.OSSettingsVerifying,
		fleet.OSSettingsPending, fleet.OSSettingsFailed,
	}
	for _, status := range statuses {
		s.checkListHostsFilter(t, allHostsLblID, "os_settings", string(status))
	}

	// all disk encryption counts are expected to be 0 (not enabled)
	s.checkMDMDiskEncryptionSummaries(t, nil, fleet.MDMDiskEncryptionSummary{}, false)

	// filter hosts with any Disk Encryption status, should have none
	diskStatuses := []fleet.DiskEncryptionStatus{
		fleet.DiskEncryptionVerified, fleet.DiskEncryptionVerifying,
		fleet.DiskEncryptionActionRequired, fleet.DiskEncryptionEnforcing,
		fleet.DiskEncryptionFailed, fleet.DiskEncryptionRemovingEnforcement,
	}
	for _, status := range diskStatuses {
		s.checkListHostsFilter(t, allHostsLblID, "os_settings_disk_encryption", string(status))
	}

	// now turn disk encryption on
	s.Do("POST", "/api/latest/fleet/disk_encryption", updateDiskEncryptionRequest{EnableDiskEncryption: true}, http.StatusNoContent)
	acResp = appConfigResponse{}
	s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp)
	assert.True(t, acResp.MDM.EnableDiskEncryption.Value)

	// the 2 LUKS-supported linux hosts should show up
	s.checkMDMProfilesSummaries(t, nil, fleet.MDMProfilesSummary{Pending: 2}, nil)
	s.checkListHostsFilter(t, allHostsLblID, "os_settings", string(fleet.OSSettingsPending), linuxHost1.ID, linuxHost2.ID)
	s.checkMDMDiskEncryptionSummaries(t, nil, fleet.MDMDiskEncryptionSummary{ActionRequired: fleet.MDMPlatformsCounts{Linux: 2}}, false)
	s.checkListHostsFilter(t, allHostsLblID, "os_settings_disk_encryption", string(fleet.DiskEncryptionActionRequired), linuxHost1.ID, linuxHost2.ID)
}

func pluckHostIDsFromHostResponse(hosts []fleet.HostResponse) []uint {
	if len(hosts) == 0 {
		return nil
	}

	ids := make([]uint, len(hosts))
	for i, h := range hosts {
		ids[i] = h.ID
	}
	return ids
}

// verifies that ListHosts, CountHosts and ListHostsInLabel all return the
// expected host IDs when run with the provided filter.
func (s *withServer) checkListHostsFilter(t *testing.T, allHostsLabelID uint, filter, value string, expectIDs ...uint) {
	listHostsRes := listHostsResponse{}
	s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listHostsRes, filter, value)
	require.Len(t, listHostsRes.Hosts, len(expectIDs), value)
	require.ElementsMatch(t, expectIDs, pluckHostIDsFromHostResponse(listHostsRes.Hosts), value)

	countResp := countHostsResponse{}
	s.DoJSON("GET", "/api/latest/fleet/hosts/count", nil, http.StatusOK, &countResp, filter, value)
	require.Equal(t, len(expectIDs), countResp.Count, value)

	listHostsRes = listHostsResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/labels/%d/hosts", allHostsLabelID), nil, http.StatusOK, &listHostsRes, filter, value)
	require.Len(t, listHostsRes.Hosts, len(expectIDs), value)
	require.ElementsMatch(t, expectIDs, pluckHostIDsFromHostResponse(listHostsRes.Hosts), value)

	// The other hosts-filter-related endpoints (delete by filter, transfer by
	// filter, hosts report) all use either ListHosts or ListsHostsInLabel.
}

// Test for #27231
func (s *integrationMDMTestSuite) TestRecreateDeletedIPhoneBYOD() {
	t := s.T()
	s.setSkipWorkerJobs(t)

	// create a team with its enroll secret
	var specResp applyTeamSpecsResponse
	teamSecret := "team_secret"
	teamSpecs := applyTeamSpecsRequest{Specs: []*fleet.TeamSpec{{Name: "newteam", Secrets: &[]fleet.EnrollSecret{{Secret: teamSecret}}}}}
	s.DoJSON("POST", "/api/latest/fleet/spec/teams", teamSpecs, http.StatusOK, &specResp)

	// enroll a BYOD iPhone
	model := "iPhone14,6"
	mdmDevice := mdmtest.NewTestMDMClientAppleOTA(s.server.URL, teamSecret, model)
	require.NoError(t, mdmDevice.Enroll())

	// get its corresponding host
	listHostsRes := listHostsResponse{}
	s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listHostsRes)
	require.Len(t, listHostsRes.Hosts, 1)
	host := listHostsRes.Hosts[0]
	require.Equal(t, mdmDevice.UUID, host.UUID)
	require.Equal(t, mdmDevice.SerialNumber, host.HardwareSerial)
	require.NotNil(t, host.TeamID)

	// delete the host
	var delResp deleteHostResponse
	s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &delResp)

	listHostsRes = listHostsResponse{}
	s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listHostsRes)
	require.Len(t, listHostsRes.Hosts, 0)

	// run the reviver cron job, will send a push notification to the deleted host
	oriPushFunc := s.pushProvider.PushFunc
	t.Cleanup(func() { s.pushProvider.PushFunc = oriPushFunc })

	var recordedPushes []*mdm.Push
	var pushMutex sync.Mutex
	s.pushProvider.PushFunc = func(ctx context.Context, pushes []*mdm.Push) (map[string]*push.Response, error) {
		pushMutex.Lock()
		recordedPushes = pushes
		pushMutex.Unlock()
		return mockSuccessfulPush(ctx, pushes)
	}
	err := apple_mdm.IOSiPadOSRevive(context.Background(), s.ds, s.mdmCommander, s.logger)
	require.NoError(t, err)
	pushMutex.Lock()
	require.Len(t, recordedPushes, 1)
	pushMutex.Unlock()

	// do an MDM sync (as the host would when triggered by the notification),
	// will re-create the host and move it to its enrollment team
	cmd, err := mdmDevice.Idle()
	require.NoError(t, err)
	require.Nil(t, cmd) // no command to process

	listHostsRes = listHostsResponse{}
	s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listHostsRes)
	require.Len(t, listHostsRes.Hosts, 1)
	require.Equal(t, mdmDevice.UUID, listHostsRes.Hosts[0].UUID)
	require.Equal(t, mdmDevice.SerialNumber, listHostsRes.Hosts[0].HardwareSerial)
	require.Equal(t, host.TeamID, listHostsRes.Hosts[0].TeamID)
}

// Test for #22391
func (s *integrationMDMTestSuite) TestRecreateDeletedIPhoneADE() {
	t := s.T()
	s.setSkipWorkerJobs(t)

	device := godep.Device{SerialNumber: "IOS0_SERIAL", Model: "iPhone 16 Pro", OS: "ios", DeviceFamily: "iPhone", OpType: "added"}

	s.enableABM("fleet_ade_test")
	s.mockDEPResponse("fleet_ade_test", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		encoder := json.NewEncoder(w)
		switch r.URL.Path {
		case "/session":
			err := encoder.Encode(map[string]string{"auth_session_token": "xyz"})
			require.NoError(t, err)
		case "/profile":
			err := encoder.Encode(godep.ProfileResponse{ProfileUUID: uuid.New().String()})
			require.NoError(t, err)
		case "/server/devices":
			err := encoder.Encode(godep.DeviceResponse{Devices: []godep.Device{device}})
			require.NoError(t, err)
		case "/devices/sync":
			// This endpoint is polled over time to sync devices from
			// ABM, send a repeated serial and a new one
			err := encoder.Encode(godep.DeviceResponse{Devices: []godep.Device{device}, Cursor: "foo"})
			require.NoError(t, err)
		case "/profile/devices":
			b, err := io.ReadAll(r.Body)
			require.NoError(t, err)

			var prof profileAssignmentReq
			require.NoError(t, json.Unmarshal(b, &prof))

			var resp godep.ProfileResponse
			resp.ProfileUUID = prof.ProfileUUID
			resp.Devices = make(map[string]string, len(prof.Devices))
			for _, device := range prof.Devices {
				resp.Devices[device] = string(fleet.DEPAssignProfileResponseSuccess)
			}
			err = encoder.Encode(resp)
			require.NoError(t, err)
		default:
			_, _ = w.Write([]byte(`{}`))
		}
	}))

	// trigger a profile sync
	s.runDEPSchedule()

	// host now exists in phantom (pending enrollment) form
	listHostsRes := listHostsResponse{}
	s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listHostsRes)
	require.Len(t, listHostsRes.Hosts, 1)
	host := listHostsRes.Hosts[0].Host
	require.Equal(t, device.SerialNumber, host.HardwareSerial)
	require.Nil(t, host.TeamID)
	require.Empty(t, host.UUID)
	require.NotNil(t, host.MDM.EnrollmentStatus)
	require.Equal(t, "Pending", *host.MDM.EnrollmentStatus)

	// enroll the host
	depURLToken := loadEnrollmentProfileDEPToken(t, s.ds)
	mdmDevice := mdmtest.NewTestMDMClientAppleDEP(s.server.URL, depURLToken)
	mdmDevice.Model = device.Model
	mdmDevice.SerialNumber = device.SerialNumber
	err := mdmDevice.Enroll()
	require.NoError(t, err)

	// host now exists as a full host
	listHostsRes = listHostsResponse{}
	s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listHostsRes)
	require.Len(t, listHostsRes.Hosts, 1)
	host = listHostsRes.Hosts[0].Host
	require.Equal(t, device.SerialNumber, host.HardwareSerial)
	require.Nil(t, host.TeamID)
	require.NotEmpty(t, host.UUID)
	require.NotNil(t, host.MDM.EnrollmentStatus)
	require.Equal(t, "On (automatic)", *host.MDM.EnrollmentStatus)

	// delete the host
	var delResp deleteHostResponse
	s.DoJSON("DELETE", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &delResp)

	// host still exists, but back in phantom (pending enrollment) form
	listHostsRes = listHostsResponse{}
	s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listHostsRes)
	require.Len(t, listHostsRes.Hosts, 1)
	host = listHostsRes.Hosts[0].Host
	require.Equal(t, device.SerialNumber, host.HardwareSerial)
	require.Nil(t, host.TeamID)
	require.NotNil(t, host.MDM.EnrollmentStatus)
	require.Equal(t, "Pending", *host.MDM.EnrollmentStatus)

	// Some time later, the refetcher will send commands to refetch that host.
	// Mimic this by setting its detail_updated_at at least 1h in the past and
	// triggering the refetch cron job.
	mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
		_, err := q.ExecContext(context.Background(), `UPDATE hosts SET detail_updated_at = DATE_SUB(NOW(), INTERVAL 2 HOUR) WHERE id = ?`, host.ID)
		return err
	})
	trigger := triggerRequest{
		Name: string(fleet.CronAppleMDMIPhoneIPadRefetcher),
	}
	s.Do("POST", "/api/latest/fleet/trigger", trigger, http.StatusOK)

	// Wait until MDM commands are set up
	done := make(chan struct{})
	go func() {
		ticker := time.NewTicker(100 * time.Millisecond)
		defer ticker.Stop()
		for range ticker.C {
			commands, err := s.ds.GetHostMDMCommands(context.Background(), host.ID)
			require.NoError(t, err)
			if len(commands) > 0 {
				done <- struct{}{}
				return
			}
		}
	}()
	select {
	case <-done:
	case <-time.After(10 * time.Second):
		t.Error("Timeout: MDM commands not queued up")
	}

	// do an MDM sync of the expected commands, will re-enroll the host
	cmd, err := mdmDevice.Idle()
	require.NoError(t, err)
	for cmd != nil {
		switch cmd.Command.RequestType {
		case "InstalledApplicationList":
			cmd, err = mdmDevice.AcknowledgeInstalledApplicationList(mdmDevice.UUID, cmd.CommandUUID, []fleet.Software{})
			require.NoError(t, err)
		case "CertificateList":
			cmd, err = mdmDevice.AcknowledgeCertificateList(mdmDevice.UUID, cmd.CommandUUID, []*x509.Certificate{})
			require.NoError(t, err)
		case "DeviceInformation":
			cmd, err = mdmDevice.AcknowledgeDeviceInformation(mdmDevice.UUID, cmd.CommandUUID, "Test Name", "iPhone 16")
			require.NoError(t, err)
		default:
			require.Fail(t, "unexpected command", cmd.Command.RequestType)
		}
	}

	listHostsRes = listHostsResponse{}
	s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listHostsRes)
	require.Len(t, listHostsRes.Hosts, 1)
	host = listHostsRes.Hosts[0].Host
	require.Equal(t, device.SerialNumber, host.HardwareSerial)
	require.Nil(t, host.TeamID)
	require.NotEmpty(t, host.UUID)
	require.NotNil(t, host.MDM.EnrollmentStatus)
	require.Equal(t, "On (automatic)", *host.MDM.EnrollmentStatus)
}

func (s *integrationMDMTestSuite) TestCancelUpcomingActivity() {
	t := s.T()
	s.setSkipWorkerJobs(t)

	// Set up VPP token
	orgName := "Fleet Device Management Inc."
	token := "validtoken"
	expTime := time.Now().Add(200 * time.Hour).UTC().Round(time.Second)
	expDate := expTime.Format(fleet.VPPTimeFormat)
	tokenJSON := fmt.Sprintf(`{"expDate":"%s","token":"%s","orgName":"%s"}`, expDate, token, orgName)
	t.Setenv("FLEET_DEV_VPP_URL", s.appleVPPConfigSrv.URL)
	var validToken uploadVPPTokenResponse
	s.uploadDataViaForm("/api/latest/fleet/vpp_tokens", "token", "token.vpptoken", []byte(base64.StdEncoding.EncodeToString([]byte(tokenJSON))), http.StatusAccepted, "", &validToken)

	// Get the token
	var resp getVPPTokensResponse
	s.DoJSON("GET", "/api/latest/fleet/vpp_tokens", &getVPPTokensRequest{}, http.StatusOK, &resp)
	require.NoError(t, resp.Err)

	mdmHost, mdmDevice := createHostThenEnrollMDM(s.ds, s.server.URL, t)
	key := setOrbitEnrollment(t, mdmHost, s.ds)
	mdmHost.OrbitNodeKey = &key

	s.runWorker()
	checkInstallFleetdCommandSent(t, mdmDevice, true)

	// Add serial number to our fake Apple server
	s.appleVPPConfigSrvConfig.SerialNumbers = append(s.appleVPPConfigSrvConfig.SerialNumbers, mdmHost.HardwareSerial)

	// Associate no team to the VPP token.
	var resPatchVPP patchVPPTokensTeamsResponse
	s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/vpp_tokens/%d/teams", resp.Tokens[0].ID), patchVPPTokensTeamsRequest{TeamIDs: []uint{}}, http.StatusOK, &resPatchVPP)

	// add VPP app
	var appResp getAppStoreAppsResponse
	s.DoJSON("GET", "/api/latest/fleet/software/app_store_apps", nil, http.StatusOK, &appResp, "team_id", "0")
	require.NoError(t, appResp.Err)
	require.True(t, len(appResp.AppStoreApps) > 0)
	addedApp := appResp.AppStoreApps[0]

	var addedMacOSApp addAppStoreAppResponse
	s.DoJSON("POST", "/api/latest/fleet/software/app_store_apps", &addAppStoreAppRequest{
		TeamID:     nil,
		Platform:   addedApp.Platform,
		AppStoreID: addedApp.AdamID,
	}, http.StatusOK, &addedMacOSApp)

	// Get software title ID of the added VPP app
	listSWTitlesResp := listSoftwareTitlesResponse{}
	s.DoJSON("GET", "/api/latest/fleet/software/titles", listSoftwareTitlesRequest{},
		http.StatusOK, &listSWTitlesResp, "query", addedApp.Name, "team_id", "0")
	require.Len(t, listSWTitlesResp.SoftwareTitles, 1)
	require.NotNil(t, listSWTitlesResp.SoftwareTitles[0].AppStoreApp)
	vppAppTitleID := listSWTitlesResp.SoftwareTitles[0].ID

	// create a software installer
	payload := &fleet.UploadSoftwareInstallerPayload{
		InstallScript: "install",
		Filename:      "dummy_installer.pkg",
		Title:         "DummyApp",
	}
	s.uploadSoftwareInstaller(t, payload, http.StatusOK, "")
	swTitleID := getSoftwareTitleID(t, s.ds, payload.Title, "apps")

	// create a saved script
	script, err := s.ds.NewScript(t.Context(), &fleet.Script{Name: "test-script.sh", ScriptContents: "echo"})
	require.NoError(t, err)

	// enqueue a VPP app install and software install
	// sleep in-between to ensure deterministic ordering
	s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/install", mdmHost.ID, vppAppTitleID), &installSoftwareRequest{}, http.StatusAccepted)
	time.Sleep(time.Millisecond)
	s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/install", mdmHost.ID, swTitleID), &installSoftwareRequest{}, http.StatusAccepted)
	time.Sleep(time.Millisecond)

	// check that all are upcoming, get the exec IDs
	var hostActivitiesResp listHostUpcomingActivitiesResponse
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/activities/upcoming", mdmHost.ID), nil, http.StatusOK, &hostActivitiesResp)
	require.Len(t, hostActivitiesResp.Activities, 2)

	// record a success for VPP app
	cmd, err := mdmDevice.Idle()
	require.NoError(t, err)
	assert.NotNil(t, cmd)
	for cmd != nil {
		var fullCmd micromdm.CommandPayload
		switch cmd.Command.RequestType { //nolint:gocritic // ignore singleCaseSwitch
		case "InstallApplication":
			require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd))
			cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID)
			require.NoError(t, err)
		}
	}

	s.runWorker()

	cmd, err = mdmDevice.Idle()
	require.NoError(t, err)
	for cmd != nil {
		var fullCmd micromdm.CommandPayload
		switch cmd.Command.RequestType { //nolint:gocritic // ignore singleCaseSwitch
		case "InstalledApplicationList":
			require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd))
			cmd, err = mdmDevice.AcknowledgeInstalledApplicationList(mdmDevice.UUID, cmd.CommandUUID, []fleet.Software{{Name: addedApp.Name, BundleIdentifier: addedApp.BundleIdentifier, Version: addedApp.LatestVersion, Installed: true}})
			require.NoError(t, err)
		default:
			require.Fail(t, "unexpected command type", cmd.Command.RequestType)
		}
	}

	// record a failure for software install
	s.Do("POST", "/api/fleet/orbit/software_install/result", json.RawMessage(fmt.Sprintf(`{
			"orbit_node_key": %q, "install_uuid": %q, "pre_install_condition_output": "ok", "install_script_exit_code": 1, "install_script_output": "fail"
		}`, *mdmHost.OrbitNodeKey, hostActivitiesResp.Activities[1].UUID)), http.StatusNoContent)

	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/activities/upcoming", mdmHost.ID), nil, http.StatusOK, &hostActivitiesResp)
	require.Len(t, hostActivitiesResp.Activities, 0)

	// enqueue a script run and an uninstall
	var runResp runScriptResponse
	s.DoJSON("POST", "/api/latest/fleet/scripts/run", fleet.HostScriptRequestPayload{HostID: mdmHost.ID, ScriptID: &script.ID}, http.StatusAccepted, &runResp)
	time.Sleep(time.Millisecond)
	s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/uninstall", mdmHost.ID, swTitleID), &uninstallSoftwareRequest{}, http.StatusAccepted)

	hostActivitiesResp = listHostUpcomingActivitiesResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/activities/upcoming", mdmHost.ID), nil, http.StatusOK, &hostActivitiesResp)
	require.Len(t, hostActivitiesResp.Activities, 2)

	// the script shows up for the host's script runs
	var scriptResp getHostScriptDetailsResponse
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/scripts", mdmHost.ID), nil, http.StatusOK, &scriptResp)
	require.Len(t, scriptResp.Scripts, 1)
	require.NotNil(t, scriptResp.Scripts[0].LastExecution)
	require.Equal(t, "pending", scriptResp.Scripts[0].LastExecution.Status)

	// cancel the script, confirm canceled activity
	s.Do("DELETE", fmt.Sprintf("/api/latest/fleet/hosts/%d/activities/upcoming/%s", mdmHost.ID, hostActivitiesResp.Activities[0].UUID), nil, http.StatusNoContent)
	lastCanceledActID := s.lastActivityOfTypeMatches(fleet.ActivityTypeCanceledRunScript{}.ActivityName(),
		fmt.Sprintf(`{"host_id": %d, "host_display_name": %q, "script_name": "test-script.sh"}`, mdmHost.ID, mdmHost.DisplayName()), 0)

	// record a script result post-cancelation
	var orbitPostScriptResp orbitPostScriptResultResponse
	s.DoJSON("POST", "/api/fleet/orbit/scripts/result",
		json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q, "execution_id": %q, "exit_code": 0, "output": "ok"}`, *mdmHost.OrbitNodeKey, hostActivitiesResp.Activities[0].UUID)),
		http.StatusOK, &orbitPostScriptResp)

	// must not generate a past activity
	s.lastActivityMatches(fleet.ActivityTypeCanceledRunScript{}.ActivityName(), "", lastCanceledActID)

	// cancel the uninstall, confirm canceled activity
	s.Do("DELETE", fmt.Sprintf("/api/latest/fleet/hosts/%d/activities/upcoming/%s", mdmHost.ID, hostActivitiesResp.Activities[1].UUID), nil, http.StatusNoContent)
	lastCanceledActID = s.lastActivityOfTypeMatches(fleet.ActivityTypeCanceledUninstallSoftware{}.ActivityName(),
		fmt.Sprintf(`{"host_id": %d, "host_display_name": %q, "software_title": "DummyApp", "software_title_id": %d}`, mdmHost.ID, mdmHost.DisplayName(), swTitleID), 0)

	// record an uninstall result post-cancelation
	s.DoJSON("POST", "/api/fleet/orbit/scripts/result",
		json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q, "execution_id": %q, "exit_code": 0, "output": "ok"}`, *mdmHost.OrbitNodeKey,
			hostActivitiesResp.Activities[1].UUID)),
		http.StatusOK, &orbitPostScriptResp)

	// must not generate a past activity
	s.lastActivityMatches(fleet.ActivityTypeCanceledUninstallSoftware{}.ActivityName(), "", lastCanceledActID)

	// enqueue a VPP app install and software install again
	s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/install", mdmHost.ID, vppAppTitleID), &installSoftwareRequest{}, http.StatusAccepted)
	time.Sleep(time.Millisecond)
	s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/install", mdmHost.ID, swTitleID), &installSoftwareRequest{}, http.StatusAccepted)

	hostActivitiesResp = listHostUpcomingActivitiesResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/activities/upcoming", mdmHost.ID), nil, http.StatusOK, &hostActivitiesResp)
	require.Len(t, hostActivitiesResp.Activities, 2)

	// cancel the VPP app install, confirm canceled activity
	s.Do("DELETE", fmt.Sprintf("/api/latest/fleet/hosts/%d/activities/upcoming/%s", mdmHost.ID, hostActivitiesResp.Activities[0].UUID), nil, http.StatusNoContent)
	lastCanceledActID = s.lastActivityOfTypeMatches(fleet.ActivityTypeCanceledInstallAppStoreApp{}.ActivityName(),
		fmt.Sprintf(`{"host_id": %d, "host_display_name": %q, "software_title": "App 1", "software_title_id": %d}`, mdmHost.ID, mdmHost.DisplayName(), vppAppTitleID), 0)

	// to be able to simulate the host sending a MDM result post-cancelation,
	// we need to re-activate the MDM command.
	mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
		_, err := q.ExecContext(t.Context(), `UPDATE nano_enrollment_queue SET active = 1 WHERE id = ? AND command_uuid = ?`, mdmHost.UUID, hostActivitiesResp.Activities[0].UUID)
		return err
	})

	// record a result for the VPP app post-cancelation
	cmd, err = mdmDevice.Idle()
	require.NoError(t, err)
	assert.NotNil(t, cmd)
	for cmd != nil {
		var fullCmd micromdm.CommandPayload
		switch cmd.Command.RequestType { //nolint:gocritic // ignore singleCaseSwitch
		case "InstallApplication":
			require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd))
			cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID)
			require.NoError(t, err)
		case "InstalledApplicationList":
			require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd))
			cmd, err = mdmDevice.AcknowledgeInstalledApplicationList(mdmDevice.UUID, cmd.CommandUUID, []fleet.Software{{Name: addedApp.Name, BundleIdentifier: addedApp.BundleIdentifier, Version: addedApp.LatestVersion, Installed: true}})
			require.NoError(t, err)
		default:
			require.Fail(t, "unexpected command", cmd.Command.RequestType)
		}
	}
	// must not generate a past activity
	s.lastActivityMatches(fleet.ActivityTypeCanceledInstallAppStoreApp{}.ActivityName(), "", lastCanceledActID)

	// cancel the software install, confirm canceled activity
	s.Do("DELETE", fmt.Sprintf("/api/latest/fleet/hosts/%d/activities/upcoming/%s", mdmHost.ID, hostActivitiesResp.Activities[1].UUID), nil, http.StatusNoContent)
	lastCanceledActID = s.lastActivityOfTypeMatches(fleet.ActivityTypeCanceledInstallSoftware{}.ActivityName(),
		fmt.Sprintf(`{"host_id": %d, "host_display_name": %q, "software_title": "DummyApp", "software_title_id": %d}`, mdmHost.ID, mdmHost.DisplayName(), swTitleID), 0)

	// record a software install result post-cancelation
	s.Do("POST", "/api/fleet/orbit/software_install/result", json.RawMessage(fmt.Sprintf(`{
			"orbit_node_key": %q, "install_uuid": %q, "pre_install_condition_output": "ok", "install_script_exit_code": 1, "install_script_output": "fail"
		}`, *mdmHost.OrbitNodeKey, hostActivitiesResp.Activities[1].UUID)), http.StatusNoContent)

	// must not generate a past activity
	s.lastActivityMatches(fleet.ActivityTypeCanceledInstallSoftware{}.ActivityName(), "", lastCanceledActID)

	// the VPP install count only sees the successful VPP app install
	titleResponse := getSoftwareTitleResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/software/titles/%d", vppAppTitleID), nil, http.StatusOK, &titleResponse, "team_id", "0")
	require.NotNil(t, titleResponse.SoftwareTitle.AppStoreApp)
	require.Equal(t, uint(1), titleResponse.SoftwareTitle.AppStoreApp.Status.Installed)
	require.Equal(t, uint(0), titleResponse.SoftwareTitle.AppStoreApp.Status.Failed)
	require.Equal(t, uint(0), titleResponse.SoftwareTitle.AppStoreApp.Status.Pending)

	// the SW install count only sees the failed install
	titleResponse = getSoftwareTitleResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/software/titles/%d", swTitleID), nil, http.StatusOK, &titleResponse, "team_id", "0")
	require.NotNil(t, titleResponse.SoftwareTitle.SoftwarePackage)
	require.Equal(t, uint(0), titleResponse.SoftwareTitle.SoftwarePackage.Status.Installed)
	require.Equal(t, uint(1), titleResponse.SoftwareTitle.SoftwarePackage.Status.FailedInstall)
	require.Equal(t, uint(0), titleResponse.SoftwareTitle.SoftwarePackage.Status.FailedUninstall)
	require.Equal(t, uint(0), titleResponse.SoftwareTitle.SoftwarePackage.Status.PendingInstall)
	require.Equal(t, uint(0), titleResponse.SoftwareTitle.SoftwarePackage.Status.PendingUninstall)

	// status of software is the one before cancellation
	getHostSoftwareResp := getHostSoftwareResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/software", mdmHost.ID), nil, http.StatusOK, &getHostSoftwareResp, "available_for_install", "1")
	require.Len(t, getHostSoftwareResp.Software, 2)
	for _, sw := range getHostSoftwareResp.Software {
		switch sw.ID {
		case vppAppTitleID:
			require.NotNil(t, sw.Status)
			require.Equal(t, fleet.SoftwareInstalled, *sw.Status)
		case swTitleID:
			require.NotNil(t, sw.Status)
			require.Equal(t, fleet.SoftwareInstallFailed, *sw.Status)
		default:
			require.Failf(t, "unexpected software title", "%d %s", sw.ID, sw.Name)
		}
	}

	// script has no last execution (only execution was canceled)
	scriptResp = getHostScriptDetailsResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/scripts", mdmHost.ID), nil, http.StatusOK, &scriptResp)
	require.Len(t, scriptResp.Scripts, 1)
	require.Nil(t, scriptResp.Scripts[0].LastExecution)
}

func (s *integrationMDMTestSuite) TestCancelLockWipeUpcomingActivity() {
	t := s.T()
	s.setSkipWorkerJobs(t)

	// create a couple linux hosts, which are locked/wiped via scripts
	h1 := createOrbitEnrolledHost(t, "ubuntu", "h1", s.ds)
	h2 := createOrbitEnrolledHost(t, "ubuntu", "h2", s.ds)

	// enqueue a lock and a wipe respectively as immediate upcoming activities
	var lockResp lockHostResponse
	s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/lock", h1.ID), nil, http.StatusOK, &lockResp)
	require.Equal(t, fleet.DeviceStatusUnlocked, lockResp.DeviceStatus)
	require.Equal(t, fleet.PendingActionLock, lockResp.PendingAction)
	s.lastActivityMatches(fleet.ActivityTypeLockedHost{}.ActivityName(), "", 0)

	var wipeResp wipeHostResponse
	s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/wipe", h2.ID), nil, http.StatusOK, &wipeResp)
	require.Equal(t, fleet.DeviceStatusUnlocked, wipeResp.DeviceStatus)
	require.Equal(t, fleet.PendingActionWipe, wipeResp.PendingAction)
	s.lastActivityMatches(fleet.ActivityTypeWipedHost{}.ActivityName(), "", 0)

	// check that all are upcoming, get the exec IDs
	var hostActivitiesResp listHostUpcomingActivitiesResponse
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/activities/upcoming", h1.ID), nil, http.StatusOK, &hostActivitiesResp)
	require.Len(t, hostActivitiesResp.Activities, 1)
	lockExecID := hostActivitiesResp.Activities[0].UUID

	hostActivitiesResp = listHostUpcomingActivitiesResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/activities/upcoming", h2.ID), nil, http.StatusOK, &hostActivitiesResp)
	require.Len(t, hostActivitiesResp.Activities, 1)
	wipeExecID := hostActivitiesResp.Activities[0].UUID

	// trying to cancel those activities fail, as the action could've started
	res := s.Do("DELETE", fmt.Sprintf("/api/latest/fleet/hosts/%d/activities/upcoming/%s", h1.ID, lockExecID), nil, http.StatusBadRequest)
	msg := extractServerErrorText(res.Body)
	require.Contains(t, msg, "Lock and wipe can't be canceled if they're about to run")
	res = s.Do("DELETE", fmt.Sprintf("/api/latest/fleet/hosts/%d/activities/upcoming/%s", h2.ID, wipeExecID), nil, http.StatusBadRequest)
	msg = extractServerErrorText(res.Body)
	require.Contains(t, msg, "Lock and wipe can't be canceled if they're about to run")

	// create a couple more linux hosts, which are locked/wiped via scripts
	h3 := createOrbitEnrolledHost(t, "ubuntu", "h3", s.ds)
	h4 := createOrbitEnrolledHost(t, "ubuntu", "h4", s.ds)

	// this time enqueue a script before the lock/wipe for each host, so that lock/wipe are cancelable
	var runResp runScriptResponse
	s.DoJSON("POST", "/api/latest/fleet/scripts/run", fleet.HostScriptRequestPayload{HostID: h3.ID, ScriptContents: "echo"}, http.StatusAccepted, &runResp)
	s.DoJSON("POST", "/api/latest/fleet/scripts/run", fleet.HostScriptRequestPayload{HostID: h4.ID, ScriptContents: "echo"}, http.StatusAccepted, &runResp)
	time.Sleep(time.Millisecond)

	s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/lock", h3.ID), nil, http.StatusOK, &lockResp)
	require.Equal(t, fleet.DeviceStatusUnlocked, lockResp.DeviceStatus)
	require.Equal(t, fleet.PendingActionLock, lockResp.PendingAction)
	lockActID := s.lastActivityMatches(fleet.ActivityTypeLockedHost{}.ActivityName(), "", 0)

	s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/wipe", h4.ID), nil, http.StatusOK, &wipeResp)
	require.Equal(t, fleet.DeviceStatusUnlocked, wipeResp.DeviceStatus)
	require.Equal(t, fleet.PendingActionWipe, wipeResp.PendingAction)
	wipeActID := s.lastActivityMatches(fleet.ActivityTypeWipedHost{}.ActivityName(), "", 0)

	// check that all are upcoming, get the exec IDs
	hostActivitiesResp = listHostUpcomingActivitiesResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/activities/upcoming", h3.ID), nil, http.StatusOK, &hostActivitiesResp)
	require.Len(t, hostActivitiesResp.Activities, 2)
	lockExecID = hostActivitiesResp.Activities[1].UUID

	hostActivitiesResp = listHostUpcomingActivitiesResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/activities/upcoming", h4.ID), nil, http.StatusOK, &hostActivitiesResp)
	require.Len(t, hostActivitiesResp.Activities, 2)
	wipeExecID = hostActivitiesResp.Activities[1].UUID

	// cancel the lock and wipe succeeds
	s.Do("DELETE", fmt.Sprintf("/api/latest/fleet/hosts/%d/activities/upcoming/%s", h3.ID, lockExecID), nil, http.StatusNoContent)
	s.Do("DELETE", fmt.Sprintf("/api/latest/fleet/hosts/%d/activities/upcoming/%s", h4.ID, wipeExecID), nil, http.StatusNoContent)

	// host lock/wipe status should've been reset
	var getHostResp getHostResponse
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", h3.ID), nil, http.StatusOK, &getHostResp)
	require.NotNil(t, getHostResp.Host.MDM.DeviceStatus)
	require.EqualValues(t, fleet.DeviceStatusUnlocked, *getHostResp.Host.MDM.DeviceStatus)
	require.NotNil(t, getHostResp.Host.MDM.PendingAction)
	require.EqualValues(t, fleet.PendingActionNone, *getHostResp.Host.MDM.PendingAction)

	getHostResp = getHostResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", h4.ID), nil, http.StatusOK, &getHostResp)
	require.NotNil(t, getHostResp.Host.MDM.DeviceStatus)
	require.EqualValues(t, fleet.DeviceStatusUnlocked, *getHostResp.Host.MDM.DeviceStatus)
	require.NotNil(t, getHostResp.Host.MDM.PendingAction)
	require.EqualValues(t, fleet.PendingActionNone, *getHostResp.Host.MDM.PendingAction)

	// past lock/wipe actions have been cleared
	var listAct listActivitiesResponse
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/activities", h3.ID), nil, http.StatusOK, &listAct)
	// latest activity is the cancelation
	require.True(t, len(listAct.Activities) > 0)
	require.Equal(t, fleet.ActivityTypeCanceledRunScript{}.ActivityName(), listAct.Activities[0].Type)
	if len(listAct.Activities) > 1 {
		require.NotEqual(t, lockActID, listAct.Activities[1].ID)
		require.NotEqual(t, fleet.ActivityTypeLockedHost{}.ActivityName(), listAct.Activities[1].Type)
	}

	listAct = listActivitiesResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/activities", h4.ID), nil, http.StatusOK, &listAct)
	require.True(t, len(listAct.Activities) > 0)
	require.Equal(t, fleet.ActivityTypeCanceledRunScript{}.ActivityName(), listAct.Activities[0].Type)
	if len(listAct.Activities) > 1 {
		require.NotEqual(t, wipeActID, listAct.Activities[1].ID)
		require.NotEqual(t, fleet.ActivityTypeWipedHost{}.ActivityName(), listAct.Activities[1].Type)
	}
}

func (s *integrationMDMTestSuite) TestSoftwareCategories() {
	t := s.T()
	ctx := context.Background()

	// Enroll a host
	token := "good_token"
	host := createOrbitEnrolledHost(t, "ubuntu", "host1", s.ds)
	createDeviceTokenForHost(t, s.ds, host.ID, token)

	payload := &fleet.UploadSoftwareInstallerPayload{
		InstallScript: "install",
		Filename:      "ruby.deb",
		Version:       "1:2.5.1",
		SelfService:   true,
	}

	// Add some categories
	cat1, err := s.ds.NewSoftwareCategory(ctx, "test_category_1")
	require.NoError(t, err)

	cat2, err := s.ds.NewSoftwareCategory(ctx, "test_category_2")
	require.NoError(t, err)

	cat3, err := s.ds.NewSoftwareCategory(ctx, "test_category_3")
	require.NoError(t, err)

	s.uploadSoftwareInstaller(t, payload, http.StatusOK, "")
	titleID := getSoftwareTitleID(t, s.ds, "ruby", "deb_packages")
	require.NotEmpty(t, titleID)

	res := s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token+"/software?self_service=1", nil, http.StatusOK)
	getDeviceSw := getDeviceSoftwareResponse{}
	err = json.NewDecoder(res.Body).Decode(&getDeviceSw)
	require.NoError(t, err)
	require.Len(t, getDeviceSw.Software, 1)
	require.Equal(t, getDeviceSw.Software[0].Name, "ruby")
	require.Nil(t, getDeviceSw.Software[0].AppStoreApp)
	require.NotNil(t, getDeviceSw.Software[0].SoftwarePackage)
	require.NotNil(t, getDeviceSw.Software[0].SoftwarePackage.SelfService)
	require.True(t, *getDeviceSw.Software[0].SoftwarePackage.SelfService)
	require.Equal(t, payload.Filename, getDeviceSw.Software[0].SoftwarePackage.Name)
	require.Equal(t, payload.Version, getDeviceSw.Software[0].SoftwarePackage.Version)
	require.Len(t, getDeviceSw.Software[0].SoftwarePackage.Categories, 0)

	updatePayload := &fleet.UpdateSoftwareInstallerPayload{
		TitleID:       titleID,
		Filename:      "ruby.deb",
		InstallScript: ptr.String("install"),
		Version:       "1:2.5.1",
		SelfService:   ptr.Bool(true),
	}

	// Non-existent category, should fail
	updatePayload.Categories = []string{"not_found"}
	s.updateSoftwareInstaller(t, updatePayload, http.StatusBadRequest, "some or all of the categories provided don't exist")

	// Set some real categories
	updatePayload.Categories = []string{cat1.Name, cat2.Name, cat2.Name} // duplicate category shouldn't fail
	s.updateSoftwareInstaller(t, updatePayload, http.StatusOK, "")
	res = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token+"/software?self_service=1", nil, http.StatusOK)
	getDeviceSw = getDeviceSoftwareResponse{}
	err = json.NewDecoder(res.Body).Decode(&getDeviceSw)
	require.NoError(t, err)
	require.Len(t, getDeviceSw.Software, 1)
	require.Equal(t, getDeviceSw.Software[0].Name, "ruby")
	require.Nil(t, getDeviceSw.Software[0].AppStoreApp)
	require.NotNil(t, getDeviceSw.Software[0].SoftwarePackage)
	require.NotNil(t, getDeviceSw.Software[0].SoftwarePackage.SelfService)
	require.True(t, *getDeviceSw.Software[0].SoftwarePackage.SelfService)
	require.Equal(t, payload.Filename, getDeviceSw.Software[0].SoftwarePackage.Name)
	require.Equal(t, payload.Version, getDeviceSw.Software[0].SoftwarePackage.Version)
	require.Len(t, getDeviceSw.Software[0].SoftwarePackage.Categories, 2)
	require.Contains(t, getDeviceSw.Software[0].SoftwarePackage.Categories, cat1.Name)
	require.Contains(t, getDeviceSw.Software[0].SoftwarePackage.Categories, cat2.Name)

	// Enqueue an install. Changing categories should not affect the status of this install
	var installResp installSoftwareResponse
	s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/install",
		host.ID, titleID), nil, http.StatusAccepted, &installResp)

	getHostSw := getHostSoftwareResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/software", host.ID), nil, http.StatusOK, &getHostSw)
	require.Len(t, getHostSw.Software, 1)
	require.Equal(t, getHostSw.Software[0].Name, "ruby")
	require.NotNil(t, getHostSw.Software[0].SoftwarePackage)
	require.Equal(t, "ruby.deb", getHostSw.Software[0].SoftwarePackage.Name)
	require.NotNil(t, getHostSw.Software[0].Status)
	require.Equal(t, fleet.SoftwareInstallPending, *getHostSw.Software[0].Status)
	require.NotNil(t, getHostSw.Software[0].SoftwarePackage.SelfService)
	require.True(t, *getHostSw.Software[0].SoftwarePackage.SelfService)

	// Switch to a different category (should remove the other 2)
	updatePayload.Categories = []string{cat3.Name}
	s.updateSoftwareInstaller(t, updatePayload, http.StatusOK, "")

	res = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token+"/software?self_service=1", nil, http.StatusOK)
	getDeviceSw = getDeviceSoftwareResponse{}
	err = json.NewDecoder(res.Body).Decode(&getDeviceSw)
	require.NoError(t, err)
	require.Len(t, getDeviceSw.Software, 1)
	require.Equal(t, getDeviceSw.Software[0].Name, "ruby")
	require.Nil(t, getDeviceSw.Software[0].AppStoreApp)
	require.NotNil(t, getDeviceSw.Software[0].SoftwarePackage)
	require.NotNil(t, getDeviceSw.Software[0].SoftwarePackage.SelfService)
	require.True(t, *getDeviceSw.Software[0].SoftwarePackage.SelfService)
	require.Equal(t, payload.Filename, getDeviceSw.Software[0].SoftwarePackage.Name)
	require.Equal(t, payload.Version, getDeviceSw.Software[0].SoftwarePackage.Version)
	require.Len(t, getDeviceSw.Software[0].SoftwarePackage.Categories, 1)
	require.Equal(t, cat3.Name, getDeviceSw.Software[0].SoftwarePackage.Categories[0])

	getHostSw = getHostSoftwareResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/software", host.ID), nil, http.StatusOK, &getHostSw)
	require.Len(t, getHostSw.Software, 1)
	require.Equal(t, getHostSw.Software[0].Name, "ruby")
	require.NotNil(t, getHostSw.Software[0].SoftwarePackage)
	require.Equal(t, "ruby.deb", getHostSw.Software[0].SoftwarePackage.Name)
	require.NotNil(t, getHostSw.Software[0].Status)
	require.Equal(t, fleet.SoftwareInstallPending, *getHostSw.Software[0].Status) // status is the same as above
	require.NotNil(t, getHostSw.Software[0].SoftwarePackage.SelfService)
	require.True(t, *getHostSw.Software[0].SoftwarePackage.SelfService)

	// Now add a VPP app

	orgName := "Fleet Device Management Inc."
	expTime := time.Now().Add(200 * time.Hour).UTC().Round(time.Second)
	expDate := expTime.Format(fleet.VPPTimeFormat)
	tokenJSON := fmt.Sprintf(`{"expDate":"%s","token":"%s","orgName":"%s"}`, expDate, token, orgName)
	t.Setenv("FLEET_DEV_VPP_URL", s.appleVPPConfigSrv.URL)
	var validToken uploadVPPTokenResponse
	s.uploadDataViaForm("/api/latest/fleet/vpp_tokens", "token", "token.vpptoken", []byte(base64.StdEncoding.EncodeToString([]byte(tokenJSON))), http.StatusAccepted, "", &validToken)

	// Get the token
	var tokenResp getVPPTokensResponse
	s.DoJSON("GET", "/api/latest/fleet/vpp_tokens", &getVPPTokensRequest{}, http.StatusOK, &tokenResp)
	require.NoError(t, tokenResp.Err)

	// Associate team to the VPP token.
	var resPatchVPP patchVPPTokensTeamsResponse
	s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/vpp_tokens/%d/teams", tokenResp.Tokens[0].ID), patchVPPTokensTeamsRequest{TeamIDs: []uint{}}, http.StatusOK, &resPatchVPP)

	var appResp getAppStoreAppsResponse
	s.DoJSON("GET", "/api/latest/fleet/software/app_store_apps", nil, http.StatusOK, &appResp, "team_id", "0")
	require.NoError(t, appResp.Err)
	require.True(t, len(appResp.AppStoreApps) > 0)
	addedApp := appResp.AppStoreApps[0]

	// Add the app with cat2 and cat3
	var addedMacOSApp addAppStoreAppResponse
	s.DoJSON("POST", "/api/latest/fleet/software/app_store_apps", &addAppStoreAppRequest{
		TeamID:     nil,
		Platform:   addedApp.Platform,
		AppStoreID: addedApp.AdamID,
		Categories: []string{cat2.Name, cat3.Name, cat3.Name}, // duplicate category shouldn't fail
	}, http.StatusOK, &addedMacOSApp)

	// get the title ID
	listSWTitlesResp := listSoftwareTitlesResponse{}
	s.DoJSON("GET", "/api/latest/fleet/software/titles", listSoftwareTitlesRequest{},
		http.StatusOK, &listSWTitlesResp, "query", addedApp.Name, "team_id", "0")
	require.Len(t, listSWTitlesResp.SoftwareTitles, 1)
	require.NotNil(t, listSWTitlesResp.SoftwareTitles[0].AppStoreApp)
	vppAppTitleID := listSWTitlesResp.SoftwareTitles[0].ID

	// get the app
	titleResponse := getSoftwareTitleResponse{}
	s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/software/titles/%d", vppAppTitleID), nil, http.StatusOK, &titleResponse, "team_id", "0")
	require.NotNil(t, titleResponse.SoftwareTitle.AppStoreApp)
	require.Len(t, titleResponse.SoftwareTitle.AppStoreApp.Categories, 2)
	require.Contains(t, titleResponse.SoftwareTitle.AppStoreApp.Categories, cat2.Name)
	require.Contains(t, titleResponse.SoftwareTitle.AppStoreApp.Categories, cat3.Name)

	// update the app and change categories
	updateAppReq := &updateAppStoreAppRequest{TeamID: nil, SelfService: ptr.Bool(true)}
	updateAppReq.Categories = []string{cat1.Name, cat3.Name}
	var updateAppResp updateAppStoreAppResponse
	s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/software/titles/%d/app_store_app", vppAppTitleID), updateAppReq, http.StatusOK, &updateAppResp)
	require.Equal(t, updateAppResp.AppStoreApp.AdamID, addedApp.AdamID)
	require.True(t, updateAppResp.AppStoreApp.SelfService)
	require.Equal(t, fleet.MacOSPlatform, updateAppResp.AppStoreApp.Platform)
	require.Contains(t, updateAppResp.AppStoreApp.Categories, cat1.Name)
	require.Contains(t, updateAppResp.AppStoreApp.Categories, cat3.Name)

	// do some gitops checks
	batchURL := "/api/latest/fleet/software/app_store_apps/batch"
	var batchAssociateResponse batchAssociateAppStoreAppsResponse
	s.DoJSON("POST",
		batchURL,
		batchAssociateAppStoreAppsRequest{
			Apps: []fleet.VPPBatchPayload{
				{AppStoreID: addedApp.AdamID, SelfService: true, Categories: []string{"Developer tools", "Communication", "Communication"}}, // duplicate category shouldn't fail
			},
		}, http.StatusOK, &batchAssociateResponse,
	)

	s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/software/titles/%d", vppAppTitleID), nil, http.StatusOK, &titleResponse, "team_id", "0")
	require.NotNil(t, titleResponse.SoftwareTitle.AppStoreApp)
	require.ElementsMatch(t, []string{"Developer tools", "Communication"}, titleResponse.SoftwareTitle.AppStoreApp.Categories)

	// empty out categories via gitops
	s.DoJSON("POST",
		batchURL,
		batchAssociateAppStoreAppsRequest{
			Apps: []fleet.VPPBatchPayload{
				{AppStoreID: addedApp.AdamID, SelfService: true},
			},
		}, http.StatusOK, &batchAssociateResponse,
	)

	s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/software/titles/%d", vppAppTitleID), nil, http.StatusOK, &titleResponse, "team_id", "0")
	require.NotNil(t, titleResponse.SoftwareTitle.AppStoreApp)
	require.Empty(t, titleResponse.SoftwareTitle.AppStoreApp.Categories)
}

// createTestServerWithStatusCode creates a local HTTP test server that always returns the specified status code.
// The server should be closed after use by calling the returned cleanup function.
func createTestServerWithStatusCode(statusCode int) (*httptest.Server, func()) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(statusCode)
	}))

	cleanup := func() {
		server.Close()
	}

	return server, cleanup
}

func (s *integrationMDMTestSuite) TestServiceDiscovery() {
	t := s.T()

	type serviceDiscoveryServer struct {
		Version string `json:"Version"`
		BaseURL string `json:"BaseURL"`
	}
	type serviceDiscoveryResponse struct {
		Servers []serviceDiscoveryServer `json:"Servers"`
	}

	res := serviceDiscoveryResponse{}
	s.DoJSON("GET", "/mdm/apple/service_discovery", nil, http.StatusOK, &res)
	require.Contains(t, res.Servers[0].BaseURL, apple_mdm.AccountDrivenEnrollPath)
	require.Equal(t, "mdm-byod", res.Servers[0].Version)
}

func (s *integrationMDMTestSuite) TestBYODEnrollmentWithIdPEnabled() {
	t := s.T()

	if !hasBuildTag("full") {
		t.Skip("This test requires running with -tags full")
	}

	ctx := t.Context()
	s.setSkipWorkerJobs(t)

	s.setUpMDMSSO(t)

	// create a couple teams, one with IdP enabled and one without
	teamNoIdP, err := s.ds.NewTeam(ctx, &fleet.Team{Name: "team without idp"})
	require.NoError(t, err)
	teamIdP, err := s.ds.NewTeam(ctx, &fleet.Team{Name: "team with idp"})
	require.NoError(t, err)
	teamIdP.Config.MDM.MacOSSetup.EnableEndUserAuthentication = true
	_, err = s.ds.SaveTeam(ctx, teamIdP)
	require.NoError(t, err)

	err = s.ds.ApplyEnrollSecrets(ctx, &teamNoIdP.ID, []*fleet.EnrollSecret{{Secret: "noidp"}})
	require.NoError(t, err)
	err = s.ds.ApplyEnrollSecrets(ctx, &teamIdP.ID, []*fleet.EnrollSecret{{Secret: "idp"}})
	require.NoError(t, err)

	// try to BYOD-enroll in team without IdP enabled, should render the download
	// profile page immediately (200)
	res := s.DoRawNoAuth("GET", "/enroll", nil, http.StatusOK, "enroll_secret", "noidp")
	page, err := io.ReadAll(res.Body)
	require.NoError(t, err)
	require.Contains(t, string(page), "Enroll your Android device in Fleet")

	// try to BYOD-enroll in team with IdP enabled, should redirect to SSO login
	res = s.DoRawNoAuth("GET", "/enroll", nil, http.StatusSeeOther, "enroll_secret", "idp")
	location := res.Header.Get("Location")
	require.NotEmpty(t, location)
	require.True(t, strings.HasPrefix(location, "http://localhost:9080/simplesaml/"))

	res = s.LoginMDMSSOUser("sso_user", "user123#")
	require.Equal(t, http.StatusSeeOther, res.StatusCode)
	location = res.Header.Get("Location")
	require.NotEmpty(t, location)
	require.True(t, strings.HasPrefix(location, "/mdm/sso/callback"))

	// requesting the /enroll page again and simulating the BYOD IdP cookie being set
	// still redirects to the SSO login if the cookie value does not match the
	// enrollment reference query string.
	res = s.DoRawWithHeaders("GET", "/enroll", nil, http.StatusSeeOther,
		map[string]string{"Cookie": shared_mdm.BYODIdpCookieName + "=abc"}, "enroll_secret", "idp", "enrollment_reference", "not_matching!")
	location = res.Header.Get("Location")
	require.NotEmpty(t, location)
	require.True(t, strings.HasPrefix(location, "http://localhost:9080/simplesaml/"))

	// requesting the /enroll page again and simulating the BYOD IdP cookie being
	// set renders the download profile page when there is a matching enrollment
	// reference.
	res = s.DoRawWithHeaders("GET", "/enroll", nil, http.StatusOK,
		map[string]string{"Cookie": shared_mdm.BYODIdpCookieName + "=abc"}, "enroll_secret", "idp", "enrollment_reference", "abc")
	page, err = io.ReadAll(res.Body)
	require.NoError(t, err)
	require.Contains(t, string(page), "Enroll your Android device in Fleet")

	// try to BYOD-enroll with invalid enroll secret, should redirect to SSO
	// login as at least one team has it enabled
	res = s.DoRawNoAuth("GET", "/enroll", nil, http.StatusSeeOther, "enroll_secret", "no-such-secret")
	location = res.Header.Get("Location")
	require.NotEmpty(t, location)
	require.True(t, strings.HasPrefix(location, "http://localhost:9080/simplesaml/"))
}

func (s *integrationMDMTestSuite) TestIOSiPadOSRefetch() {
	ctx := s.T().Context()

	successfulPushUUID := "successful-uuid"
	failedPushUUID := "failed-uuid"

	// Set up test data

	// Perform the MDM enrollment.
	mdmEnrollInfo := mdmtest.AppleEnrollInfo{
		SCEPChallenge: s.scepChallenge,
		SCEPURL:       s.server.URL + apple_mdm.SCEPPath,
		MDMURL:        s.server.URL + apple_mdm.MDMPath,
	}
	model := "iPhone14,6"

	successfulHost, err := s.ds.NewHost(ctx, &fleet.Host{
		HardwareSerial:  mdmtest.RandSerialNumber(),
		UUID:            successfulPushUUID,
		Platform:        string(fleet.IOSPlatform),
		DetailUpdatedAt: time.Now().Add(-2 * time.Hour), // ensure refetch is needed
	})
	require.NoError(s.T(), err)

	successfulMdmDevice := mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo, model)
	successfulMdmDevice.SerialNumber = successfulHost.HardwareSerial
	err = successfulMdmDevice.Enroll()
	require.NoError(s.T(), err)

	failedHost, err := s.ds.NewHost(ctx, &fleet.Host{
		HardwareSerial:  mdmtest.RandSerialNumber(),
		UUID:            failedPushUUID,
		Platform:        string(fleet.IOSPlatform),
		DetailUpdatedAt: time.Now().Add(-2 * time.Hour), // ensure refetch is needed
	})
	require.NoError(s.T(), err)

	failedMdmDevice := mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo, model)
	failedMdmDevice.SerialNumber = failedHost.HardwareSerial
	err = failedMdmDevice.Enroll()
	require.NoError(s.T(), err)

	failedHostTokenInactive, err := s.ds.NewHost(ctx, &fleet.Host{
		HardwareSerial:  mdmtest.RandSerialNumber(),
		UUID:            failedPushUUID,
		Platform:        string(fleet.IOSPlatform),
		DetailUpdatedAt: time.Now().Add(-2 * time.Hour), // ensure refetch is needed
	})
	require.NoError(s.T(), err)

	failedMdmDeviceTokenInactive := mdmtest.NewTestMDMClientAppleDirect(mdmEnrollInfo, model)
	failedMdmDeviceTokenInactive.SerialNumber = failedHostTokenInactive.HardwareSerial
	err = failedMdmDeviceTokenInactive.Enroll()
	require.NoError(s.T(), err)

	originalPushFunc := s.pushProvider.PushFunc
	s.T().Cleanup(func() {
		s.pushProvider.PushFunc = originalPushFunc
	})
	s.pushProvider.PushFunc = func(_ context.Context, pushes []*mdm.Push) (map[string]*push.Response, error) {
		require.Len(s.T(), pushes, 1)
		pushObject := pushes[0]
		switch pushObject.PushMagic {
		case "pushmagic" + successfulMdmDevice.SerialNumber:
			return map[string]*push.Response{
				pushObject.Token.String(): {
					Id:  successfulPushUUID,
					Err: nil,
				},
			}, nil
		case "pushmagic" + failedMdmDeviceTokenInactive.SerialNumber:
			return map[string]*push.Response{
				pushObject.Token.String(): {
					Id:  failedPushUUID,
					Err: errors.New("device token is inactive"),
				},
			}, nil
		case "pushmagic" + failedMdmDevice.SerialNumber:
			return map[string]*push.Response{
				pushObject.Token.String(): {
					Id:  failedPushUUID,
					Err: errors.New("random apns error"),
				},
			}, nil
		}
		return nil, errors.New("unknown device")
	}

	err = apple_mdm.IOSiPadOSRefetch(ctx, s.ds, s.mdmCommander, s.logger, func(ctx context.Context, user *fleet.User, act fleet.ActivityDetails) error {
		return newActivity(ctx, user, act, s.ds, s.logger)
	})
	require.NoError(s.T(), err) // Verify it not longer throws an error

	// Verify successful is still enrolled
	successfulHostMDM, err := s.ds.GetHostMDM(ctx, successfulHost.ID)
	require.NoError(s.T(), err)
	require.NotNil(s.T(), successfulHostMDM)
	require.True(s.T(), successfulHostMDM.Enrolled)

	// Verify random APNS error host is still enrolled
	failedHostMDM, err := s.ds.GetHostMDM(ctx, failedHost.ID)
	require.NoError(s.T(), err)
	require.NotNil(s.T(), failedHostMDM)
	require.True(s.T(), failedHostMDM.Enrolled)

	// Verify device token inactive host is no longer enrolled
	failedHostMDMTokenInactive, err := s.ds.GetHostMDM(ctx, failedHostTokenInactive.ID)
	require.NoError(s.T(), err)
	require.NotNil(s.T(), failedHostMDMTokenInactive)
	require.False(s.T(), failedHostMDMTokenInactive.Enrolled)
}

// for https://github.com/fleetdm/fleet/issues/29086
func (s *integrationMDMTestSuite) TestWipeWindowsReenrollAsNewHost() {
	t := s.T()
	ctx := context.Background()

	// create an MDM-enrolled Windows host
	host, winMDMClient := createWindowsHostThenEnrollMDM(s.ds, s.server.URL, t)

	// update its serial number to empty, to simulate the DreamQuest device (and
	// others) where this can happen
	host.HardwareSerial = ""
	err := s.ds.UpdateHost(ctx, host)
	require.NoError(t, err)

	// get the host's information
	var getHostResp getHostResponse
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &getHostResp)
	require.NotNil(t, getHostResp.Host.MDM.DeviceStatus)
	require.Equal(t, "unlocked", *getHostResp.Host.MDM.DeviceStatus)
	require.NotNil(t, getHostResp.Host.MDM.PendingAction)
	require.Equal(t, "", *getHostResp.Host.MDM.PendingAction)

	// wipe the host
	var wipeResp wipeHostResponse
	s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/wipe", host.ID), nil, http.StatusOK, &wipeResp)
	require.Equal(t, fleet.PendingActionWipe, wipeResp.PendingAction)
	require.Equal(t, fleet.DeviceStatusUnlocked, wipeResp.DeviceStatus)

	// refresh the host's status, it is unlocked, pending wipe
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &getHostResp)
	require.NotNil(t, getHostResp.Host.MDM.DeviceStatus)
	require.Equal(t, string(fleet.DeviceStatusUnlocked), *getHostResp.Host.MDM.DeviceStatus)
	require.NotNil(t, getHostResp.Host.MDM.PendingAction)
	require.Equal(t, string(fleet.PendingActionWipe), *getHostResp.Host.MDM.PendingAction)

	status, err := s.ds.GetHostLockWipeStatus(ctx, host)
	require.NoError(t, err)
	// simulate a successful wipe from the Windows device's MDM response
	cmds, err := winMDMClient.StartManagementSession()
	require.NoError(t, err)

	// two status + the wipe command we enqueued
	require.Len(t, cmds, 3)
	wipeCmd := cmds[status.WipeMDMCommand.CommandUUID]
	require.NotNil(t, wipeCmd)
	require.Equal(t, wipeCmd.Verb, fleet.CmdExec)
	require.Len(t, wipeCmd.Cmd.Items, 1)
	require.EqualValues(t, "./Device/Vendor/MSFT/RemoteWipe/doWipeProtected", *wipeCmd.Cmd.Items[0].Target)

	msgID, err := winMDMClient.GetCurrentMsgID()
	require.NoError(t, err)

	winMDMClient.AppendResponse(fleet.SyncMLCmd{
		XMLName: xml.Name{Local: fleet.CmdStatus},
		MsgRef:  &msgID,
		CmdRef:  &status.WipeMDMCommand.CommandUUID,
		Cmd:     ptr.String("Exec"),
		Data:    ptr.String("200"),
		Items:   nil,
		CmdID:   fleet.CmdID{Value: uuid.NewString()},
	})
	cmds, err = winMDMClient.SendResponse()
	require.NoError(t, err)
	// the ack of the message should be the only returned command
	require.Len(t, cmds, 1)

	// refresh the host's status, it is wiped
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &getHostResp)
	require.NotNil(t, getHostResp.Host.MDM.DeviceStatus)
	require.Equal(t, string(fleet.DeviceStatusWiped), *getHostResp.Host.MDM.DeviceStatus)
	require.NotNil(t, getHostResp.Host.MDM.PendingAction)
	require.Equal(t, string(fleet.PendingActionNone), *getHostResp.Host.MDM.PendingAction)

	// enroll a new host that will re-enroll as the same host for Windows MDM
	// (via the same hardware device ID).
	newHost := createOrbitEnrolledHost(t, "windows", uuid.NewString(), s.ds)
	require.NotEqual(t, host.ID, newHost.ID)

	// now re-enroll in MDM for the new host but with the same hardware device ID
	newHostDevice := mdmtest.NewTestMDMClientWindowsProgramatic(s.server.URL, *newHost.OrbitNodeKey)
	newHostDevice.HardwareID = winMDMClient.HardwareID
	err = newHostDevice.Enroll()
	require.NoError(t, err)
	updated, err := s.ds.UpdateMDMWindowsEnrollmentsHostUUID(ctx, newHost.UUID, newHostDevice.DeviceID)
	require.NoError(t, err)
	// The MDM Enrollment should have linked the host UUID to the Windows enrollment so there should have been no update
	require.False(t, updated)
	err = s.ds.SetOrUpdateMDMData(ctx, newHost.ID, false, true, s.server.URL, false, fleet.WellKnownMDMFleet, "", false)
	require.NoError(t, err)

	// refresh the (old) host's status, it should not 500
	s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &getHostResp)
	require.NotNil(t, getHostResp.Host.MDM.DeviceStatus)
	require.Equal(t, string(fleet.DeviceStatusUnlocked), *getHostResp.Host.MDM.DeviceStatus)
	require.NotNil(t, getHostResp.Host.MDM.PendingAction)
	require.Equal(t, string(fleet.PendingActionNone), *getHostResp.Host.MDM.PendingAction)

	// attempting to wipe the old host entry should fail, as it is not reported
	// as enrolled in Fleet MDM anymore
	s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/wipe", host.ID), nil, http.StatusUnprocessableEntity, &wipeResp)

	// attempting to wipe the new host entry works
	s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/wipe", newHost.ID), nil, http.StatusOK, &wipeResp)
	require.Equal(t, fleet.PendingActionWipe, wipeResp.PendingAction)
	require.Equal(t, fleet.DeviceStatusUnlocked, wipeResp.DeviceStatus)
}

func (s *integrationMDMTestSuite) TestAndroidEnterpriseDeletedDetection() {
	t := s.T()
	ctx := t.Context()

	// ----- SETUP

	appConfig, err := s.ds.AppConfig(ctx)
	require.NoError(t, err)
	originalAppConfig := *appConfig
	appConfig.MDM.AndroidEnabledAndConfigured = false
	err = s.ds.SaveAppConfig(ctx, appConfig)
	require.NoError(t, err)

	t.Cleanup(func() {
		err := s.ds.SaveAppConfig(context.Background(), &originalAppConfig)
		require.NoError(t, err)
	})

	// ---- SETUP DONE

	s.androidAPIClient.InitCommonMocks()

	var proxyCallbackURL string
	s.androidAPIClient.SignupURLsCreateFunc = func(_ context.Context, _, callbackURL string) (*android.SignupDetails, error) {
		url, err := url.Parse(callbackURL)
		require.NoError(t, err)
		proxyCallbackURL = url.EscapedPath() + "?" + url.RawQuery
		return &android.SignupDetails{
			Url:  tests.EnterpriseSignupURL,
			Name: "signupUrls/Cb0812d0999c464f",
		}, nil
	}
	s.androidAPIClient.EnterprisesCreateFunc = func(_ context.Context, _ androidmgmt.EnterprisesCreateRequest) (androidmgmt.EnterprisesCreateResponse, error) {
		return androidmgmt.EnterprisesCreateResponse{
			EnterpriseName: "enterprises/" + tests.EnterpriseID,
			TopicName:      "projects/android/topics/ae98ed130-5ce2-4ddb-a90a-191ec76976d5",
		}, nil
	}
	s.androidAPIClient.EnterprisesListFunc = func(_ context.Context, _ string) ([]*androidmanagement.Enterprise, error) {
		return []*androidmanagement.Enterprise{}, nil
	}

	// First, create an enterprise to test with
	var signupResp android.EnterpriseSignupResponse
	s.DoJSON("GET", "/api/latest/fleet/android_enterprise/signup_url", nil, http.StatusOK, &signupResp)

	const enterpriseToken = "enterpriseToken"
	s.Do("GET", proxyCallbackURL, nil, http.StatusOK, "enterpriseToken", enterpriseToken)

	// Update LIST mock to return the enterprise after "creation"
	s.androidAPIClient.EnterprisesListFunc = func(_ context.Context, _ string) ([]*androidmanagement.Enterprise, error) {
		return []*androidmanagement.Enterprise{
			{Name: "enterprises/" + tests.EnterpriseID},
		}, nil
	}

	// Verify enterprise exists
	var resp android.GetEnterpriseResponse
	s.DoJSON("GET", "/api/latest/fleet/android_enterprise", nil, http.StatusOK, &resp)
	assert.Equal(t, tests.EnterpriseID, resp.EnterpriseID)

	t.Run("enterprise deleted detection", func(t *testing.T) {
		// Mock EnterprisesListFunc to return empty list (enterprise deleted)
		s.androidAPIClient.EnterprisesListFunc = func(_ context.Context, _ string) ([]*androidmanagement.Enterprise, error) {
			return []*androidmanagement.Enterprise{}, nil
		}

		// Attempt to get enterprise - should return 200 until cron is ran
		var getResp android.GetEnterpriseResponse
		s.DoJSON("GET", "/api/latest/fleet/android_enterprise", nil, http.StatusOK, &getResp)
		// Verify LIST API was not called
		assert.False(t, s.androidAPIClient.EnterprisesListFuncInvoked)

		// Trigger cron
		s.awaitRunCleanupSchedule()

		// Verify LIST API was called
		assert.True(t, s.androidAPIClient.EnterprisesListFuncInvoked)

		// Attempt to get enterprise - should now return 404 since cron detected deletion
		s.DoJSON("GET", "/api/v1/fleet/android_enterprise", nil, http.StatusNotFound, &getResp)
	})
}
